diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 3321aae3c0994..a2cda1e0b1e87 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -19,7 +19,7 @@ More details are available in the https://www.typescriptlang.org/docs/handbook/p ==== Caveats This architecture imposes several limitations to which we must comply: -- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. +- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. https://github.com/elastic/kibana/issues/78162 is going to provide a tool to find such problem places. - A project must emit its type declaration. It's not always possible to generate a type declaration if the compiler cannot infer a type. There are two basic cases: 1. Your plugin exports a type inferring an internal type declared in Kibana codebase. In this case, you'll have to either export an internal type or to declare an exported type explicitly. @@ -27,7 +27,8 @@ This architecture imposes several limitations to which we must comply: [discrete] ==== Prerequisites -Since `tsc` doesn't support circular project references, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. +Since project refs rely on generated `d.ts` files, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. +https://github.com/elastic/kibana/issues/79343 is going to provide a tool for identifying a plugin dependency tree. [discrete] ==== Implementation diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md new file mode 100644 index 0000000000000..5616064ddaa0a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getValueBucketPath](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) + +## AggConfig.getValueBucketPath() method + +Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) + +Signature: + +```typescript +getValueBucketPath(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md index ceb90cffbf6ca..d4a8eddf51cfc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md @@ -47,6 +47,7 @@ export declare class AggConfig | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md) | | | | [getTimeRange()](./kibana-plugin-plugins-data-public.aggconfig.gettimerange.md) | | | | [getValue(bucket)](./kibana-plugin-plugins-data-public.aggconfig.getvalue.md) | | | +| [getValueBucketPath()](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) | | Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) | | [isFilterable()](./kibana-plugin-plugins-data-public.aggconfig.isfilterable.md) | | | | [makeLabel(percentageMode)](./kibana-plugin-plugins-data-public.aggconfig.makelabel.md) | | | | [nextId(list)](./kibana-plugin-plugins-data-public.aggconfig.nextid.md) | static | Calculate the next id based on the ids in this list {array} list - a list of objects with id properties | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md deleted file mode 100644 index 6c8594b7eeffd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) - -## expandShorthand variable - - -Signature: - -```typescript -expandShorthand: (sh: Record) => MappingObject -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md deleted file mode 100644 index 3e8b0abec529c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) - -## FieldMappingSpec.\_deserialize property - -Signature: - -```typescript -_deserialize?: (mapping: string) => any | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md deleted file mode 100644 index d0aaf7ddd0c17..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) - -## FieldMappingSpec.\_serialize property - -Signature: - -```typescript -_serialize?: (mapping: any) => string | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md deleted file mode 100644 index 38ebe60df99a1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) - -## FieldMappingSpec interface - - -Signature: - -```typescript -export interface FieldMappingSpec -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) | (mapping: string) => any | undefined | | -| [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) | (mapping: any) => string | undefined | | -| [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) | ES_FIELD_TYPES | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md deleted file mode 100644 index 73cff623dc7f2..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) - -## FieldMappingSpec.type property - -Signature: - -```typescript -type: ES_FIELD_TYPES; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md deleted file mode 100644 index b1f33c8e8546d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) - -## MappingObject type - - -Signature: - -```typescript -export declare type MappingObject = Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index f8897a059377d..6a3c437305cc8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -59,7 +59,6 @@ | [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | Data plugin prewired UI components | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | -| [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | @@ -108,7 +107,6 @@ | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | -| [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | @@ -163,7 +161,6 @@ | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | -| [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 9c47ea1a166d5..b99c5f0f10a9e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -16,6 +16,6 @@ export interface ISearchStartAggsStart | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | -| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | ISearchStrategy['search'] | | | [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md index fdcd4d6768db5..98ea175aaaea7 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; +search: ISearchStrategy['search']; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index 3d2caf417f3cb..6dd95da2be3c1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -17,5 +17,5 @@ export interface ISearchStrategy(context: RequestHandlerContext, id: string) => Promise<void> | | -| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable<SearchStrategyResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md index 45f43648ab603..84b90ae23f916 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; +search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md new file mode 100644 index 0000000000000..6f30bd49013b0 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.extract.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [extract](./kibana-plugin-plugins-expressions-public.executor.extract.md) + +## Executor.extract() method + +Signature: + +```typescript +extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | + +Returns: + +`{ + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md new file mode 100644 index 0000000000000..8f5a8a3e06724 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.inject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [inject](./kibana-plugin-plugins-expressions-public.executor.inject.md) + +## Executor.inject() method + +Signature: + +```typescript +inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| references | SavedObjectReference[] | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index b71c8c79c068f..2f96ad6e040bd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> +export declare class Executor = Record> implements PersistableState ``` ## Constructors @@ -32,12 +32,15 @@ export declare class Executor = Recordstatic | | | [extendContext(extraContext)](./kibana-plugin-plugins-expressions-public.executor.extendcontext.md) | | | +| [extract(ast)](./kibana-plugin-plugins-expressions-public.executor.extract.md) | | | | [fork()](./kibana-plugin-plugins-expressions-public.executor.fork.md) | | | | [getFunction(name)](./kibana-plugin-plugins-expressions-public.executor.getfunction.md) | | | | [getFunctions()](./kibana-plugin-plugins-expressions-public.executor.getfunctions.md) | | | | [getType(name)](./kibana-plugin-plugins-expressions-public.executor.gettype.md) | | | | [getTypes()](./kibana-plugin-plugins-expressions-public.executor.gettypes.md) | | | +| [inject(ast, references)](./kibana-plugin-plugins-expressions-public.executor.inject.md) | | | | [registerFunction(functionDefinition)](./kibana-plugin-plugins-expressions-public.executor.registerfunction.md) | | | | [registerType(typeDefinition)](./kibana-plugin-plugins-expressions-public.executor.registertype.md) | | | | [run(ast, input, context)](./kibana-plugin-plugins-expressions-public.executor.run.md) | | Execute expression and return result. | +| [telemetry(ast, telemetryData)](./kibana-plugin-plugins-expressions-public.executor.telemetry.md) | | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md new file mode 100644 index 0000000000000..de4c640f4fe97 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.telemetry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [telemetry](./kibana-plugin-plugins-expressions-public.executor.telemetry.md) + +## Executor.telemetry() method + +Signature: + +```typescript +telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| telemetryData | Record<string, any> | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md deleted file mode 100644 index b50ac83036ffe..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) > [chain](./kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md) - -## ExpressionAstExpression.chain property - -Signature: - -```typescript -chain: ExpressionAstFunction[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md index 537659c51dce8..623c49bf08cdd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.md @@ -2,18 +2,13 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) -## ExpressionAstExpression interface +## ExpressionAstExpression type Signature: ```typescript -export interface ExpressionAstExpression +export declare type ExpressionAstExpression = { + type: 'expression'; + chain: ExpressionAstFunction[]; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [chain](./kibana-plugin-plugins-expressions-public.expressionastexpression.chain.md) | ExpressionAstFunction[] | | -| [type](./kibana-plugin-plugins-expressions-public.expressionastexpression.type.md) | 'expression' | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md deleted file mode 100644 index 72b44e8319542..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [arguments](./kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md) - -## ExpressionAstFunction.arguments property - -Signature: - -```typescript -arguments: Record; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md deleted file mode 100644 index 36101a110979a..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [debug](./kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md) - -## ExpressionAstFunction.debug property - -Debug information added to each function when expression is executed in \*debug mode\*. - -Signature: - -```typescript -debug?: ExpressionAstFunctionDebug; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md deleted file mode 100644 index 1840fff4b625f..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.function.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [function](./kibana-plugin-plugins-expressions-public.expressionastfunction.function.md) - -## ExpressionAstFunction.function property - -Signature: - -```typescript -function: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md index 1004e58759806..d21f2c1750161 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) -## ExpressionAstFunction interface +## ExpressionAstFunction type Signature: ```typescript -export interface ExpressionAstFunction +export declare type ExpressionAstFunction = { + type: 'function'; + function: string; + arguments: Record; + debug?: ExpressionAstFunctionDebug; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [arguments](./kibana-plugin-plugins-expressions-public.expressionastfunction.arguments.md) | Record<string, ExpressionAstArgument[]> | | -| [debug](./kibana-plugin-plugins-expressions-public.expressionastfunction.debug.md) | ExpressionAstFunctionDebug | Debug information added to each function when expression is executed in \*debug mode\*. | -| [function](./kibana-plugin-plugins-expressions-public.expressionastfunction.function.md) | string | | -| [type](./kibana-plugin-plugins-expressions-public.expressionastfunction.type.md) | 'function' | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md similarity index 52% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md index f7f8786430191..f07d5b3b36d04 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastfunction.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) > [type](./kibana-plugin-plugins-expressions-public.expressionastfunction.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [disabled](./kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md) -## ExpressionAstFunction.type property +## ExpressionFunction.disabled property Signature: ```typescript -type: 'function'; +disabled: boolean; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md new file mode 100644 index 0000000000000..c5d726849cdc2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.extract.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [extract](./kibana-plugin-plugins-expressions-public.expressionfunction.extract.md) + +## ExpressionFunction.extract property + +Signature: + +```typescript +extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md new file mode 100644 index 0000000000000..6f27a6fbab96a --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.inject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) + +## ExpressionFunction.inject property + +Signature: + +```typescript +inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 5ca67e40c93ec..1815d63d804b1 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class ExpressionFunction +export declare class ExpressionFunction implements PersistableState ``` ## Constructors @@ -23,9 +23,13 @@ export declare class ExpressionFunction | [accepts](./kibana-plugin-plugins-expressions-public.expressionfunction.accepts.md) | | (type: string) => boolean | | | [aliases](./kibana-plugin-plugins-expressions-public.expressionfunction.aliases.md) | | string[] | Aliases that can be used instead of name. | | [args](./kibana-plugin-plugins-expressions-public.expressionfunction.args.md) | | Record<string, ExpressionFunctionParameter> | Specification of expression function parameters. | +| [disabled](./kibana-plugin-plugins-expressions-public.expressionfunction.disabled.md) | | boolean | | +| [extract](./kibana-plugin-plugins-expressions-public.expressionfunction.extract.md) | | (state: ExpressionAstFunction['arguments']) => {
state: ExpressionAstFunction['arguments'];
references: SavedObjectReference[];
} | | | [fn](./kibana-plugin-plugins-expressions-public.expressionfunction.fn.md) | | (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue | Function to run function (context, args) | | [help](./kibana-plugin-plugins-expressions-public.expressionfunction.help.md) | | string | A short help text. | +| [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | +| [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md new file mode 100644 index 0000000000000..249c99f50fc7b --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) + +## ExpressionFunction.telemetry property + +Signature: + +```typescript +telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md new file mode 100644 index 0000000000000..e6aefd17fceb2 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) > [disabled](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md) + +## ExpressionFunctionDefinition.disabled property + +if set to true function will be disabled (but its migrate function will still be available) + +Signature: + +```typescript +disabled?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md index bc801542f81ac..449cc66cb3335 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition ``` ## Properties @@ -19,6 +19,7 @@ export interface ExpressionFunctionDefinitionstring[] | What is this? | | [args](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.args.md) | {
[key in keyof Arguments]: ArgumentType<Arguments[key]>;
} | Specification of arguments that function supports. This list will also be used for autocomplete functionality when your function is being edited. | | [context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.context.md) | {
types: AnyExpressionFunctionDefinition['inputTypes'];
} | | +| [disabled](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.disabled.md) | boolean | if set to true function will be disabled (but its migrate function will still be available) | | [help](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.help.md) | string | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md index 3b3c1644adbef..9a2507056eb80 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md @@ -14,5 +14,6 @@ export interface ExpressionRenderError extends Error | Property | Type | Description | | --- | --- | --- | +| [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) | Error | | | [type](./kibana-plugin-plugins-expressions-public.expressionrendererror.type.md) | string | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md similarity index 51% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md index 34a86e235a911..45f74a52e6b6f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionastexpression.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) > [type](./kibana-plugin-plugins-expressions-public.expressionastexpression.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionRenderError](./kibana-plugin-plugins-expressions-public.expressionrendererror.md) > [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) -## ExpressionAstExpression.type property +## ExpressionRenderError.original property Signature: ```typescript -type: 'expression'; +original?: Error; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md new file mode 100644 index 0000000000000..90f1f59c90dea --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.extract.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [extract](./kibana-plugin-plugins-expressions-public.expressionsservice.extract.md) + +## ExpressionsService.extract property + +Extracts saved object references from expression AST + +Signature: + +```typescript +readonly extract: (state: ExpressionAstExpression) => { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md new file mode 100644 index 0000000000000..8ccc673ef24db --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.inject.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) + +## ExpressionsService.inject property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index fa93435bffc38..041d66b22dd50 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -15,7 +15,7 @@ so that JSDoc appears in developers IDE when they use those `plugins.expressions Signature: ```typescript -export declare class ExpressionsService +export declare class ExpressionsService implements PersistableState ``` ## Constructors @@ -30,6 +30,7 @@ export declare class ExpressionsService | --- | --- | --- | --- | | [execute](./kibana-plugin-plugins-expressions-public.expressionsservice.execute.md) | | ExpressionsServiceStart['execute'] | | | [executor](./kibana-plugin-plugins-expressions-public.expressionsservice.executor.md) | | Executor | | +| [extract](./kibana-plugin-plugins-expressions-public.expressionsservice.extract.md) | | (state: ExpressionAstExpression) => {
state: ExpressionAstExpression;
references: SavedObjectReference[];
} | Extracts saved object references from expression AST | | [fork](./kibana-plugin-plugins-expressions-public.expressionsservice.fork.md) | | () => ExpressionsService | | | [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.getfunction.md) | | ExpressionsServiceStart['getFunction'] | | | [getFunctions](./kibana-plugin-plugins-expressions-public.expressionsservice.getfunctions.md) | | () => ReturnType<Executor['getFunctions']> | Returns POJO map of all registered expression functions, where keys are names of the functions and values are ExpressionFunction instances. | @@ -37,6 +38,7 @@ export declare class ExpressionsService | [getRenderers](./kibana-plugin-plugins-expressions-public.expressionsservice.getrenderers.md) | | () => ReturnType<ExpressionRendererRegistry['toJS']> | Returns POJO map of all registered expression renderers, where keys are names of the renderers and values are ExpressionRenderer instances. | | [getType](./kibana-plugin-plugins-expressions-public.expressionsservice.gettype.md) | | ExpressionsServiceStart['getType'] | | | [getTypes](./kibana-plugin-plugins-expressions-public.expressionsservice.gettypes.md) | | () => ReturnType<Executor['getTypes']> | Returns POJO map of all registered expression types, where keys are names of the types and values are ExpressionType instances. | +| [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) | | (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression | Injects saved object references into expression AST | | [registerFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.registerfunction.md) | | (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void | Register an expression function, which will be possible to execute as part of the expression pipeline.Below we register a function which simply sleeps for given number of milliseconds to delay the execution and outputs its input as-is. ```ts expressions.registerFunction({ @@ -61,6 +63,7 @@ The actual function is defined in the fn key. The function can be \ | [registerType](./kibana-plugin-plugins-expressions-public.expressionsservice.registertype.md) | | (typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)) => void | | | [renderers](./kibana-plugin-plugins-expressions-public.expressionsservice.renderers.md) | | ExpressionRendererRegistry | | | [run](./kibana-plugin-plugins-expressions-public.expressionsservice.run.md) | | ExpressionsServiceStart['run'] | | +| [telemetry](./kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md) | | (state: ExpressionAstExpression, telemetryData?: Record<string, any>) => Record<string, any> | Extracts telemetry from expression AST | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md new file mode 100644 index 0000000000000..5f28eb732e389 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [telemetry](./kibana-plugin-plugins-expressions-public.expressionsservice.telemetry.md) + +## ExpressionsService.telemetry property + +Extracts telemetry from expression AST + +Signature: + +```typescript +readonly telemetry: (state: ExpressionAstExpression, telemetryData?: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md index 4a714fe62424f..1dee4a139c660 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md @@ -8,13 +8,7 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; }>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index ead6f14e0d1d7..b0c732188a46e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -55,9 +55,7 @@ | [ExecutionParams](./kibana-plugin-plugins-expressions-public.executionparams.md) | | | [ExecutionState](./kibana-plugin-plugins-expressions-public.executionstate.md) | | | [ExecutorState](./kibana-plugin-plugins-expressions-public.executorstate.md) | | -| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) | | | [ExpressionAstExpressionBuilder](./kibana-plugin-plugins-expressions-public.expressionastexpressionbuilder.md) | | -| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) | | | [ExpressionAstFunctionBuilder](./kibana-plugin-plugins-expressions-public.expressionastfunctionbuilder.md) | | | [ExpressionExecutor](./kibana-plugin-plugins-expressions-public.expressionexecutor.md) | | | [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) | ExpressionFunctionDefinition is the interface plugins have to implement to register a function in expressions plugin. | @@ -102,6 +100,8 @@ | [ExecutionContainer](./kibana-plugin-plugins-expressions-public.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-public.executorcontainer.md) | | | [ExpressionAstArgument](./kibana-plugin-plugins-expressions-public.expressionastargument.md) | | +| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-public.expressionastexpression.md) | | +| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-public.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-public.expressionastnode.md) | | | [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-public.expressionfunctionkibana.md) | | | [ExpressionRendererComponent](./kibana-plugin-plugins-expressions-public.expressionrenderercomponent.md) | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md new file mode 100644 index 0000000000000..26d1e7810f9e7 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Range](./kibana-plugin-plugins-expressions-public.range.md) > [label](./kibana-plugin-plugins-expressions-public.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md index cf0cf4cb50b71..83d4b9bd35090 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-public.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-public.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-public.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-public.range.type.md) | typeof name | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index bd6c8cba5f784..5622516530edd 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -20,5 +20,5 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | | [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | Observable<unknown> | An observable which can be used to re-run the expression without destroying the component | -| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (error?: string | null) => React.ReactElement | React.ReactElement[] | | +| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[] | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md index 48bfe1ee5c7c7..162d0da04ae7f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md @@ -7,5 +7,5 @@ Signature: ```typescript -renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; +renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md new file mode 100644 index 0000000000000..0829824732e74 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.extract.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [extract](./kibana-plugin-plugins-expressions-server.executor.extract.md) + +## Executor.extract() method + +Signature: + +```typescript +extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | + +Returns: + +`{ + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md new file mode 100644 index 0000000000000..bbc5f7a3cece7 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.inject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [inject](./kibana-plugin-plugins-expressions-server.executor.inject.md) + +## Executor.inject() method + +Signature: + +```typescript +inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| references | SavedObjectReference[] | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index 7e6bb8c7ded5e..ec4e0bdcc4569 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class Executor = Record> +export declare class Executor = Record> implements PersistableState ``` ## Constructors @@ -32,12 +32,15 @@ export declare class Executor = Recordstatic | | | [extendContext(extraContext)](./kibana-plugin-plugins-expressions-server.executor.extendcontext.md) | | | +| [extract(ast)](./kibana-plugin-plugins-expressions-server.executor.extract.md) | | | | [fork()](./kibana-plugin-plugins-expressions-server.executor.fork.md) | | | | [getFunction(name)](./kibana-plugin-plugins-expressions-server.executor.getfunction.md) | | | | [getFunctions()](./kibana-plugin-plugins-expressions-server.executor.getfunctions.md) | | | | [getType(name)](./kibana-plugin-plugins-expressions-server.executor.gettype.md) | | | | [getTypes()](./kibana-plugin-plugins-expressions-server.executor.gettypes.md) | | | +| [inject(ast, references)](./kibana-plugin-plugins-expressions-server.executor.inject.md) | | | | [registerFunction(functionDefinition)](./kibana-plugin-plugins-expressions-server.executor.registerfunction.md) | | | | [registerType(typeDefinition)](./kibana-plugin-plugins-expressions-server.executor.registertype.md) | | | | [run(ast, input, context)](./kibana-plugin-plugins-expressions-server.executor.run.md) | | Execute expression and return result. | +| [telemetry(ast, telemetryData)](./kibana-plugin-plugins-expressions-server.executor.telemetry.md) | | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md new file mode 100644 index 0000000000000..68100c38cfa5b --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.telemetry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [telemetry](./kibana-plugin-plugins-expressions-server.executor.telemetry.md) + +## Executor.telemetry() method + +Signature: + +```typescript +telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | ExpressionAstExpression | | +| telemetryData | Record<string, any> | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md deleted file mode 100644 index cc8006b918dec..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) > [chain](./kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md) - -## ExpressionAstExpression.chain property - -Signature: - -```typescript -chain: ExpressionAstFunction[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md index b5f83d1af7cb7..9606cb9e36960 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.md @@ -2,18 +2,13 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) -## ExpressionAstExpression interface +## ExpressionAstExpression type Signature: ```typescript -export interface ExpressionAstExpression +export declare type ExpressionAstExpression = { + type: 'expression'; + chain: ExpressionAstFunction[]; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [chain](./kibana-plugin-plugins-expressions-server.expressionastexpression.chain.md) | ExpressionAstFunction[] | | -| [type](./kibana-plugin-plugins-expressions-server.expressionastexpression.type.md) | 'expression' | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md deleted file mode 100644 index 46cd60cecaa84..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastexpression.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) > [type](./kibana-plugin-plugins-expressions-server.expressionastexpression.type.md) - -## ExpressionAstExpression.type property - -Signature: - -```typescript -type: 'expression'; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md deleted file mode 100644 index 052cadffb9bdb..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [arguments](./kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md) - -## ExpressionAstFunction.arguments property - -Signature: - -```typescript -arguments: Record; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md deleted file mode 100644 index b3227c2ac5822..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [debug](./kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md) - -## ExpressionAstFunction.debug property - -Debug information added to each function when expression is executed in \*debug mode\*. - -Signature: - -```typescript -debug?: ExpressionAstFunctionDebug; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md deleted file mode 100644 index 9964409f49119..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.function.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [function](./kibana-plugin-plugins-expressions-server.expressionastfunction.function.md) - -## ExpressionAstFunction.function property - -Signature: - -```typescript -function: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md index 1d49de44b571d..7fbcf2dcfd141 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.md @@ -2,20 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) -## ExpressionAstFunction interface +## ExpressionAstFunction type Signature: ```typescript -export interface ExpressionAstFunction +export declare type ExpressionAstFunction = { + type: 'function'; + function: string; + arguments: Record; + debug?: ExpressionAstFunctionDebug; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [arguments](./kibana-plugin-plugins-expressions-server.expressionastfunction.arguments.md) | Record<string, ExpressionAstArgument[]> | | -| [debug](./kibana-plugin-plugins-expressions-server.expressionastfunction.debug.md) | ExpressionAstFunctionDebug | Debug information added to each function when expression is executed in \*debug mode\*. | -| [function](./kibana-plugin-plugins-expressions-server.expressionastfunction.function.md) | string | | -| [type](./kibana-plugin-plugins-expressions-server.expressionastfunction.type.md) | 'function' | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md similarity index 52% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md index 3fd10524c1599..8ae51645f5df9 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionastfunction.type.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) > [type](./kibana-plugin-plugins-expressions-server.expressionastfunction.type.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [disabled](./kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md) -## ExpressionAstFunction.type property +## ExpressionFunction.disabled property Signature: ```typescript -type: 'function'; +disabled: boolean; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md new file mode 100644 index 0000000000000..e7ecad4a6c9e4 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.extract.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [extract](./kibana-plugin-plugins-expressions-server.expressionfunction.extract.md) + +## ExpressionFunction.extract property + +Signature: + +```typescript +extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md new file mode 100644 index 0000000000000..85c98ef9193da --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.inject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) + +## ExpressionFunction.inject property + +Signature: + +```typescript +inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index aac3878b8c859..7fcda94968d13 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class ExpressionFunction +export declare class ExpressionFunction implements PersistableState ``` ## Constructors @@ -23,9 +23,13 @@ export declare class ExpressionFunction | [accepts](./kibana-plugin-plugins-expressions-server.expressionfunction.accepts.md) | | (type: string) => boolean | | | [aliases](./kibana-plugin-plugins-expressions-server.expressionfunction.aliases.md) | | string[] | Aliases that can be used instead of name. | | [args](./kibana-plugin-plugins-expressions-server.expressionfunction.args.md) | | Record<string, ExpressionFunctionParameter> | Specification of expression function parameters. | +| [disabled](./kibana-plugin-plugins-expressions-server.expressionfunction.disabled.md) | | boolean | | +| [extract](./kibana-plugin-plugins-expressions-server.expressionfunction.extract.md) | | (state: ExpressionAstFunction['arguments']) => {
state: ExpressionAstFunction['arguments'];
references: SavedObjectReference[];
} | | | [fn](./kibana-plugin-plugins-expressions-server.expressionfunction.fn.md) | | (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue | Function to run function (context, args) | | [help](./kibana-plugin-plugins-expressions-server.expressionfunction.help.md) | | string | A short help text. | +| [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | +| [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md new file mode 100644 index 0000000000000..2894486847b27 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) + +## ExpressionFunction.telemetry property + +Signature: + +```typescript +telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md new file mode 100644 index 0000000000000..88456c8700aec --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md) > [disabled](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md) + +## ExpressionFunctionDefinition.disabled property + +if set to true function will be disabled (but its migrate function will still be available) + +Signature: + +```typescript +disabled?: boolean; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md index 6463c6ac537b9..51240f094b181 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition ``` ## Properties @@ -19,6 +19,7 @@ export interface ExpressionFunctionDefinitionstring[] | What is this? | | [args](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.args.md) | {
[key in keyof Arguments]: ArgumentType<Arguments[key]>;
} | Specification of arguments that function supports. This list will also be used for autocomplete functionality when your function is being edited. | | [context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.context.md) | {
types: AnyExpressionFunctionDefinition['inputTypes'];
} | | +| [disabled](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.disabled.md) | boolean | if set to true function will be disabled (but its migrate function will still be available) | | [help](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.help.md) | string | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md index b90e4360e055a..c8132948a8993 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md @@ -8,13 +8,7 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; }>; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md index c9fed2e00c66c..dd7c7af466bd0 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md @@ -52,9 +52,7 @@ | [ExecutionParams](./kibana-plugin-plugins-expressions-server.executionparams.md) | | | [ExecutionState](./kibana-plugin-plugins-expressions-server.executionstate.md) | | | [ExecutorState](./kibana-plugin-plugins-expressions-server.executorstate.md) | | -| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) | | | [ExpressionAstExpressionBuilder](./kibana-plugin-plugins-expressions-server.expressionastexpressionbuilder.md) | | -| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) | | | [ExpressionAstFunctionBuilder](./kibana-plugin-plugins-expressions-server.expressionastfunctionbuilder.md) | | | [ExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md) | ExpressionFunctionDefinition is the interface plugins have to implement to register a function in expressions plugin. | | [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) | A mapping of ExpressionFunctionDefinitions for functions which the Expressions services provides out-of-the-box. Any new functions registered by the Expressions plugin should have their types added here. | @@ -86,6 +84,8 @@ | [ExecutionContainer](./kibana-plugin-plugins-expressions-server.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-server.executorcontainer.md) | | | [ExpressionAstArgument](./kibana-plugin-plugins-expressions-server.expressionastargument.md) | | +| [ExpressionAstExpression](./kibana-plugin-plugins-expressions-server.expressionastexpression.md) | | +| [ExpressionAstFunction](./kibana-plugin-plugins-expressions-server.expressionastfunction.md) | | | [ExpressionAstNode](./kibana-plugin-plugins-expressions-server.expressionastnode.md) | | | [ExpressionFunctionKibana](./kibana-plugin-plugins-expressions-server.expressionfunctionkibana.md) | | | [ExpressionsServerSetup](./kibana-plugin-plugins-expressions-server.expressionsserversetup.md) | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md new file mode 100644 index 0000000000000..767f6011290a1 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Range](./kibana-plugin-plugins-expressions-server.range.md) > [label](./kibana-plugin-plugins-expressions-server.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md index d369d882757fc..4e6ae12217f2e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-server.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-server.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-server.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-server.range.type.md) | typeof name | | diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png deleted file mode 100644 index dd653bb046f45..0000000000000 Binary files a/docs/infrastructure/images/infra-sysmon.png and /dev/null differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc deleted file mode 100644 index 81a3022436a7e..0000000000000 --- a/docs/infrastructure/index.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-infra]] -= Metrics - -The {metrics-app} in {kib} enables you to monitor your infrastructure metrics and identify problems in real time. -You start with a visual summary of your infrastructure where you can view basic metrics for common servers, containers, and services. -Then you can drill down to view more detailed metrics or other information for that component. - -You can: - -* View your infrastructure metrics by hosts, Kubernetes pods, or Docker containers. -You can group and filter the data in various ways to help you identify the items that interest you. - -* View current and historic values for metrics such as CPU usage, memory usage, and network traffic for each component. -The available metrics depend on the kind of component being inspected. - -* Use *Metrics Explorer* to group and visualize multiple customizable metrics for one or more components in a graphical format. -You can optionally save these views and add them to {kibana-ref}/dashboard.html[dashboards]. - -* Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. - -* Create alerts based on metric thresholds for one or more components. - -[role="screenshot"] -image::infrastructure/images/infra-sysmon.png[Infrastructure Overview in Kibana] - -[float] -=== Get started - -To get started with Metrics, refer to {metrics-guide}/install-metrics-monitoring.html[Install Metrics]. - diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png deleted file mode 100644 index ddd3346475da6..0000000000000 Binary files a/docs/logs/images/logs-console.png and /dev/null differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc deleted file mode 100644 index 45d4321f40556..0000000000000 --- a/docs/logs/index.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-logs]] -= Logs - -The Logs app in Kibana enables you to explore logs for common servers, containers, and services. - -The Logs app has a compact, console-like display that you can customize. -You can filter the logs by various fields, start and stop live streaming, and highlight text of interest. - -You can open the Logs app from the *Logs* tab in Kibana. -You can also open the Logs app directly from a component in the Metrics app. -In this case, you will only see the logs for the selected component. - -[role="screenshot"] -image::logs/images/logs-console.png[Logs Console in Kibana] - -[float] -=== Get started - -To get started with Elastic Logs, refer to {logs-guide}/install-logs-monitoring.html[Install Logs]. diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc index 73cf40c4d7c40..f348812550978 100644 --- a/docs/management/alerting/alert-management.asciidoc +++ b/docs/management/alerting/alert-management.asciidoc @@ -4,7 +4,7 @@ beta[] -The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: +The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: * <> alerts * <> including enabling/disabling, muting/unmuting, and deleting @@ -39,7 +39,7 @@ image::images/alerts-filter-by-action-type.png[Filtering the alert list by type [[create-edit-alerts]] ==== Creating and editing alerts -Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. diff --git a/docs/management/images/index-lifecycle-policies-create.png b/docs/management/images/index-lifecycle-policies-create.png deleted file mode 100644 index f6d86fa9b4ea5..0000000000000 Binary files a/docs/management/images/index-lifecycle-policies-create.png and /dev/null differ diff --git a/docs/management/images/index_lifecycle_policies_options.png b/docs/management/images/index_lifecycle_policies_options.png deleted file mode 100644 index 184188be181e8..0000000000000 Binary files a/docs/management/images/index_lifecycle_policies_options.png and /dev/null differ diff --git a/docs/management/images/index_management_add_policy.png b/docs/management/images/index_management_add_policy.png deleted file mode 100644 index f0fe493a2e491..0000000000000 Binary files a/docs/management/images/index_management_add_policy.png and /dev/null differ diff --git a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc b/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc deleted file mode 100644 index 0fec62d895754..0000000000000 --- a/docs/management/index-lifecycle-policies/add-policy-to-index.asciidoc +++ /dev/null @@ -1,17 +0,0 @@ -[role="xpack"] -[[adding-policy-to-index]] -=== Adding a policy to an index - -To add a lifecycle policy to an index and view the status for indices -managed by a policy, open the menu, then go to *Stack Management > Data > Index Management*. -This page lists your -{es} indices, which you can filter by lifecycle status and lifecycle phase. - -To add a policy, select the index name and then select *Manage Index > Add lifecycle policy*. -You’ll see the policy name, the phase the index is in, the current -action, and if any errors occurred performing that action. - -To remove a policy from an index, select *Manage Index > Remove lifecycle policy*. - -[role="screenshot"] -image::images/index_management_add_policy.png[][UI for adding a policy to an index] diff --git a/docs/management/index-lifecycle-policies/create-policy.asciidoc b/docs/management/index-lifecycle-policies/create-policy.asciidoc deleted file mode 100644 index 7849ef6b92054..0000000000000 --- a/docs/management/index-lifecycle-policies/create-policy.asciidoc +++ /dev/null @@ -1,93 +0,0 @@ -[role="xpack"] -[[creating-index-lifecycle-policies]] -=== Creating an index lifecycle policy - -An index lifecycle policy enables you to define rules over when to perform -certain actions, such as a rollover or force merge, on an index. Index lifecycle -management automates execution of those actions at the right time. - -When you create an index lifecycle policy, consider the tradeoffs between -performance and availability. As you move your index through the lifecycle, -you’re likely moving your data to less performant hardware and reducing the -number of shards and replicas. It’s important to ensure that the index -continues to have enough replicas to prevent data loss in the event of failures. - -*Index Lifecycle Policies* is automatically enabled in {kib}. Open the menu, then go to -*Stack Management > {es} > Index Lifecycle Policies*. - -NOTE: If you don’t want to use this feature, you can disable it by setting -`xpack.ilm.enabled` to false in your `kibana.yml` configuration file. If you -disable *Index Management*, then *Index Lifecycle Policies* is also disabled. - -[role="screenshot"] -image::images/index-lifecycle-policies-create.png[][UI for creating an index lifecycle policy] - -==== Defining the phases of the index lifecycle - -You can define up to four phases in the index lifecycle. For each phase, you -can enable actions to optimize performance for that phase. - -The four phases in the index lifecycle are: - -* *Hot.* The index is actively being queried and written to. You can -roll over to a new index when the -original index reaches a specified size, document count, or age. When a rollover occurs, a new -index is created, added to the index alias, and designated as the new “hot” -index. You can still query the previous indices, but you only ever write to -the “hot” index. See <>. - -* *Warm.* The index is typically searched at a lower rate than when the data is -hot. The index is not used for storing new data, but might occasionally add -late-arriving data, for example, from a Beat with a network problem that's now fixed. -You can optionally shrink the number replicas and move the shards to a -different set of nodes with smaller or less performant hardware. You can also -reduce the number of primary shards and force merge the index into -smaller {ref}/indices-segments.html[segments]. - -* *Cold.* The index is no longer being updated and is seldom queried, but is -still searchable. If you have a big deployment, you can move it to even -less performant hardware. You might also reduce the number of replicas because -you expect the data to be queried less frequently. To keep the index searchable -for a longer period, and reduce the hardware requirements, you can use the -{ref}/frozen-indices.html[freeze action]. Queries are slower on a frozen index because the index is -reloaded from the disk to RAM on demand. - -* *Delete.* The index is no longer relevant. You can define when it is safe to -delete it. - -The index lifecycle always includes an active hot phase. The warm, cold, and -delete phases are optional. For example, you might define all four phases for -one policy and only a hot and delete phase for another. See {ref}/_actions.html[Actions] -for more information on the actions available in each phase. - -[[setting-a-rollover-action]] -==== Setting a rollover action - -The {ref}/indices-rollover-index.html[rollover] action enables you to automatically roll over to a new index based -on the index size, document count, or age. Rolling over to a new index based on -these criteria is preferable to time-based rollovers. Rolling over at an arbitrary -time often results in many small indices, which can have a negative impact on performance and resource usage. - -When you create an index lifecycle policy, the rollover action is enabled -by default. The default size for triggering the rollover is 50 gigabytes, and -the default age is 30 days. The rollover occurs when any of the criteria are met. - -With the rollover action enabled, you can move to the warm phase on rollover or you can -time the move for a specified number of hours or days after the rollover. The -move to the cold and delete phases is based on the time from the rollover. - -If you are using daily indices (created by Logstash or another client) and you -want to use the index lifecycle policy to manage aging data, you can -disable the rollover action in the hot phase. You can then -transition to the warm, cold, and delete phases based on the time of index creation. - -==== Setting the index priority - -For the hot, warm, and cold phases, you can set a priority for recovering -indices after a node restart. Indices with higher priorities are recovered -before indices with lower priorities. By default, the index priority is set to -100 in the hot phase, 50 in the warm phase, and 0 in the cold phase. -If the cold phase of one index has data that -is more important than the data in the hot phase of another, you might increase -the index priority in the cold phase. See -{ref}/recovery-prioritization.html[Index recovery prioritization]. diff --git a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc b/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc deleted file mode 100644 index ba1d79710de05..0000000000000 --- a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[role="xpack"] -[[index-lifecycle-policies]] -== Index Lifecycle Policies - -If you're working with time series data, you don't want to continually dump -everything into a single index. Instead, you might periodically roll over the -data to a new index to keep it from growing so big it's slow and expensive. -As the index ages and you query it less frequently, you’ll likely move it to -less expensive hardware and reduce the number of shards and replicas. - -To automatically move an index through its lifecycle, you can create a policy -to define actions to perform on the index as it ages. Index lifecycle policies -are especially useful when working with {beats-ref}/beats-reference.html[Beats] -data shippers, which continually -send operational data, such as metrics and logs, to Elasticsearch. You can -automate a rollover to a new index when the existing index reaches a specified -size or age. This ensures that all indices have a similar size instead of having -daily indices where size can vary based on the number of Beats and the number -of events sent. - -{kib}’s *Index Lifecycle Policies* walks you through the process for creating -and configuring a policy. Before using this feature, you should be familiar -with index lifecycle management: - -* For an introduction, refer to -{ref}/getting-started-index-lifecycle-management.html[Getting started with index -lifecycle management]. -* To dig into the concepts and technical details, see -{ref}/index-lifecycle-management.html[Managing the index lifecycle]. -* To check out the APIs, see {ref}/index-lifecycle-management-api.html[Index lifecycle management API]. diff --git a/docs/management/index-lifecycle-policies/manage-policy.asciidoc b/docs/management/index-lifecycle-policies/manage-policy.asciidoc deleted file mode 100644 index 8e2dc96de4b99..0000000000000 --- a/docs/management/index-lifecycle-policies/manage-policy.asciidoc +++ /dev/null @@ -1,34 +0,0 @@ -[role="xpack"] -[[managing-index-lifecycle-policies]] -=== Managing index lifecycle policies - -Your configured policies appear on the *Index lifecycle policies* page. -You can update an existing index lifecycle policy to fix errors or change -strategies for newly created indices. To edit a policy, select its name. - -[role="screenshot"] -image::images/index_lifecycle_policies_options.png[][UI for viewing and editing an index lifecycle policy] - -In addition, you can: - -* *View indices linked to the policy.* This is important when editing a policy. -Any changes you make affect all indices attached to the policy. The settings -for the current phase are cached, so the update doesn’t affect that phase. This -prevents conflicts when you’re modifying a phase that is currently executing on -an index. The changes takes effect when the next phase in the index lifecycle begins. - -* *Add the policy to an index template.* When an index is automatically -created using the index template, the policy is applied. If the index is rolled -over, the policies for any matching index templates are applied to the newly -created index. For more information, see {ref}/indices-templates.html[Index templates]. - -* *Delete a policy.* You can’t delete a policy that is currently in use or -recover a deleted index. - -[float] -=== Required permissions - -The `manage_ilm` cluster privilege is required to access *Index lifecycle policies*. - -You can add these privileges in *Stack Management > Security > Roles*. - diff --git a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png old mode 100755 new mode 100644 index 8d8b8aa4b42e3..2de7449affd0c Binary files a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png and b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png differ diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc index da2d3b8accac2..7986e4e56279a 100644 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc @@ -62,11 +62,40 @@ You also want to know where the request is coming from. . In *Ingest Node Pipelines*, click *Create a pipeline*. . Provide a name and description for the pipeline. -. Define the processors: +. Add a grok processor to parse the log message: + +.. Click *Add a processor* and select the *Grok* processor type. +.. Set the field input to `message` and enter the following grok pattern: + [source,js] ---------------------------------- -[ +%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent} +---------------------------------- ++ +.. Click *Update* to save the processor. + +. Add processors to map the date, IP, and user agent fields. + +.. Map the appropriate field to each processor type: ++ +-- +* **Date**: `timestamp` +* **GeoIP**: `clientip` +* **User agent**: `agent` + +For the **Date** processor, you also need to specify the date format you want to use: `dd/MMM/YYYY:HH:mm:ss Z`. +-- +Your form should look similar to this: ++ +[role="screenshot"] +image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] ++ +Alternatively, you can click the **Import processors** link and define the processors as JSON: ++ +[source,js] +---------------------------------- +{ + "processors": [ { "grok": { "field": "message", @@ -90,19 +119,16 @@ You also want to know where the request is coming from. } } ] +} ---------------------------------- + -This code defines four {ref}/ingest-processors.html[processors] that run sequentially: +The four {ref}/ingest-processors.html[processors] will run sequentially: {ref}/grok-processor.html[grok], {ref}/date-processor.html[date], -{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. -Your form should look similar to this: -+ -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] +{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. You can reorder processors using the arrow icon next to each processor. -. To verify that the pipeline gives the expected outcome, click *Test pipeline*. +. To test the pipeline to verify that it produces the expected results, click *Add documents*. -. In the *Document* tab, provide the following sample document for testing: +. In the *Documents* tab, provide a sample document for testing: + [source,js] ---------------------------------- diff --git a/docs/observability/images/apm-app.png b/docs/observability/images/apm-app.png new file mode 100644 index 0000000000000..acbaa70c7f2f1 Binary files /dev/null and b/docs/observability/images/apm-app.png differ diff --git a/docs/observability/images/logs-app.png b/docs/observability/images/logs-app.png new file mode 100644 index 0000000000000..1138ec175e5bf Binary files /dev/null and b/docs/observability/images/logs-app.png differ diff --git a/docs/observability/images/metrics-app.png b/docs/observability/images/metrics-app.png new file mode 100644 index 0000000000000..8c00a31974a70 Binary files /dev/null and b/docs/observability/images/metrics-app.png differ diff --git a/docs/observability/images/uptime-app.png b/docs/observability/images/uptime-app.png new file mode 100644 index 0000000000000..522a696adf506 Binary files /dev/null and b/docs/observability/images/uptime-app.png differ diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc index d63402e8df2fb..c924cea3712dd 100644 --- a/docs/observability/index.asciidoc +++ b/docs/observability/index.asciidoc @@ -13,12 +13,69 @@ With *Observability*, you have: * *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. * An alerts chart to keep you informed of any issues that you may need to resolve quickly. +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. + [role="screenshot"] image::observability/images/observability-overview.png[Observability Overview in {kib}] [float] -== Get started +[[logs-app]] +== Logs -{kib} provides step-by-step instructions to help you add and configure your data -sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information -and instructions. +The {logs-app} in {kib} enables you to search, filter, and tail all your logs +ingested into {es}. Instead of having to log into different servers, change +directories, and tail individual files, all your logs are available in the {logs-app}. + +There is live streaming of logs, filtering using auto-complete, and a logs histogram +for quick navigation. You can also use machine learning to detect specific log +anomalies automatically and categorize log messages to quickly identify patterns in your +log events. + +To get started with the {logs-app}, see {observability-guide}/ingest-logs.html[Ingest logs]. + +[role="screenshot"] +image::observability/images/logs-app.png[Logs app in {kib}] + +[float] +[[metrics-app]] +== Metrics + +The {metrics-app} in {kib} enables you to visualize infrastructure metrics +to help diagnose problematic spikes, identify high resource utilization, +automatically discover and track pods, and unify your metrics +with logs and APM data in {es}. + +To get started with the {metrics-app}, see {observability-guide}/ingest-metrics.html[Ingest metrics]. + +[role="screenshot"] +image::observability/images/metrics-app.png[Metrics app in {kib}] + +[float] +[[uptime-app]] +== Uptime + +The {uptime-app} in {kib} enables you to monitor the availability and response times +of applications and services in real time, and detect problems before they affect users. +You can monitor the status of network endpoints via HTTP/S, TCP, and ICMP, explore +endpoint status over time, drill down into specific monitors, and view a high-level +snapshot of your environment at any point in time. + +To get started with the {uptime-app}, see {observability-guide}/ingest-uptime.html[Ingest uptime data]. + +[role="screenshot"] +image::observability/images/uptime-app.png[Uptime app in {kib}] + +[float] +[[apm-app]] +== APM + +The APM app in {kib} enables you to monitors software services and applications in real time, +collect unhandled errors and exceptions, and automatically pick up basic host-level metrics +and agent specific metrics. + +To get started with the APM app, see <>. + +[role="screenshot"] +image::observability/images/apm-app.png[APM app in {kib}] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 92e02de89f181..d8c200450d7e5 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -113,8 +113,33 @@ This content has moved. See This content has moved. See {ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Connect to a remote cluster]. +[role="exclude",id="adding-policy-to-index"] +== Adding a policy to an index + +This content has moved. See +{ref}/set-up-lifecycle-policy.html[Configure a lifecycle policy]. + +[role="exclude",id="creating-index-lifecycle-policies"] +== Creating an index lifecycle policy + +This content has moved. See +{ref}/set-up-lifecycle-policy.html[Configure a lifecycle policy]. + +[role="exclude",id="index-lifecycle-policies"] +== Index Lifecycle Policies + +This content has moved. See +{ref}/index-lifecycle-management.html[ILM: Manage the index lifecycle]. + +[role="exclude",id="managing-index-lifecycle-policies"] +== Managing index lifecycle policies + +This content has moved. See +{ref}/index-lifecycle-management.html[ILM: Manage the index lifecycle]. + [role="exclude",id="tutorial-define-index"] == Define your index patterns This content has moved. See <>. + diff --git a/docs/uptime/images/uptime-overview.png b/docs/uptime/images/uptime-overview.png deleted file mode 100644 index 25c88b2d14287..0000000000000 Binary files a/docs/uptime/images/uptime-overview.png and /dev/null differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc deleted file mode 100644 index 66c9e9357420f..0000000000000 --- a/docs/uptime/index.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-uptime]] -= Uptime - -The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. -You can explore endpoint status over time, drill down into specific monitors, -and view a high-level snapshot of your environment at any point in time. - -[role="screenshot"] -image::images/uptime-overview.png[Uptime app overview] - -[float] -=== Get started - -To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[Install Uptime]. - - - diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 4a99c70f9d961..f71e43c5defc7 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,7 +2,7 @@ [[alert-types]] == Alert types -{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index bdb72b1658cd2..f8656b87cbe04 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. image::images/alerting-overview.png[Alerts and actions UI] @@ -148,7 +148,7 @@ Functionally, {kib} alerting differs in that: * {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. * Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. [float] @@ -170,9 +170,9 @@ If you are using an *on-premises* Elastic Stack deployment with <> -* <> +* <> * <> -* <> +* <> See <> for more information on configuring roles that provide access to these features. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 7f201d2c39e89..89a487ca8fb32 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index c186704dd2f16..d375b6f425e54 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -27,14 +27,8 @@ include::graph/index.asciidoc[] include::{kib-repo-dir}/observability/index.asciidoc[] -include::{kib-repo-dir}/logs/index.asciidoc[] - -include::{kib-repo-dir}/infrastructure/index.asciidoc[] - include::{kib-repo-dir}/apm/index.asciidoc[] -include::{kib-repo-dir}/uptime/index.asciidoc[] - include::{kib-repo-dir}/siem/index.asciidoc[] include::dev-tools.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e0d550a15a907..c371aa695c475 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -40,7 +40,7 @@ a| <> flushing, and clearing the cache. Practicing good index management ensures that your data is stored cost effectively. -| <> +| {ref}/index-lifecycle-management.html[Index Lifecycle Policies] |Create a policy for defining the lifecycle of an index as it ages through the hot, warm, cold, and delete phases. Such policies help you control operation costs @@ -180,14 +180,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/manage-policy.asciidoc[] - -include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index.asciidoc[] - include::{kib-repo-dir}/management/managing-indices.asciidoc[] include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 169982544e6e8..26e7056cdd787 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -17,6 +17,7 @@ * under the License. */ +import { map } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../src/plugins/data/server'; import { IMyStrategyResponse, IMyStrategyRequest } from '../common'; @@ -25,13 +26,13 @@ export const mySearchStrategyProvider = ( ): ISearchStrategy => { const es = data.search.getSearchStrategy('es'); return { - search: async (context, request, options): Promise => { - const esSearchRes = await es.search(context, request, options); - return { - ...esSearchRes, - cool: request.get_cool ? 'YES' : 'NOPE', - }; - }, + search: (request, options, context) => + es.search(request, options, context).pipe( + map((esSearchRes) => ({ + ...esSearchRes, + cool: request.get_cool ? 'YES' : 'NOPE', + })) + ), cancel: async (context, id) => { if (es.cancel) { es.cancel(context, id); diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts index 6eb21cf34b4a3..21ae38b99f3d2 100644 --- a/examples/search_examples/server/routes/server_search_route.ts +++ b/examples/search_examples/server/routes/server_search_route.ts @@ -39,26 +39,28 @@ export function registerServerSearchRoute(router: IRouter, data: DataPluginStart // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion. // If you wish to run the search with polling (in basic+), you'd have to poll on the search API. // Please reach out to the @app-arch-team if you need this to be implemented. - const res = await data.search.search( - context, - { - params: { - index, - body: { - aggs: { - '1': { - avg: { - field, + const res = await data.search + .search( + { + params: { + index, + body: { + aggs: { + '1': { + avg: { + field, + }, }, }, }, + waitForCompletionTimeout: '5m', + keepAlive: '5m', }, - waitForCompletionTimeout: '5m', - keepAlive: '5m', - }, - } as IEsSearchRequest, - {} - ); + } as IEsSearchRequest, + {}, + context + ) + .toPromise(); return response.ok({ body: { diff --git a/package.json b/package.json index 1e334f75d41b0..732ee1fd3038b 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "typescript": "4.0.2", "ui-select": "0.19.8", "vega": "^5.17.0", - "vega-lite": "^4.16.8", + "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.24.2", "vinyl-fs": "^3.0.3", diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index 7fbf009e38a7d..1ce3b9eeeafd0 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -64,6 +64,7 @@ export class Cache { this.codes = LmdbStore.open({ name: 'codes', path: CACHE_DIR, + maxReaders: 500, }); this.atimes = this.codes.openDB({ diff --git a/packages/kbn-test/src/jest/junit_reporter.ts b/packages/kbn-test/src/jest/junit_reporter.ts index 8deb6751bc268..0712584122e05 100644 --- a/packages/kbn-test/src/jest/junit_reporter.ts +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -22,6 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import xmlBuilder from 'xmlbuilder'; +import { REPO_ROOT } from '@kbn/utils'; import type { Config } from '@jest/types'; import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; @@ -46,7 +47,7 @@ export default class JestJUnitReporter extends BaseReporter { constructor(globalConfig: Config.GlobalConfig, { rootDirectory, reportName }: ReporterOptions) { super(); this._reportName = reportName || 'Jest Tests'; - this._rootDirectory = rootDirectory ? resolve(rootDirectory) : resolve(__dirname, '../..'); + this._rootDirectory = rootDirectory ? resolve(rootDirectory) : REPO_ROOT; } /** diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts new file mode 100644 index 0000000000000..f88cdd899ef81 --- /dev/null +++ b/src/core/public/apm_system.test.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('@elastic/apm-rum'); +import { init, apm } from '@elastic/apm-rum'; +import { ApmSystem } from './apm_system'; + +const initMock = init as jest.Mocked; +const apmMock = apm as DeeplyMockedKeys; + +describe('ApmSystem', () => { + afterEach(() => { + jest.resetAllMocks(); + jest.resetAllMocks(); + }); + + describe('setup', () => { + it('does not init apm if no config provided', async () => { + const apmSystem = new ApmSystem(undefined); + await apmSystem.setup(); + expect(initMock).not.toHaveBeenCalled(); + }); + + it('calls init with configuration', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(initMock).toHaveBeenCalledWith({ active: true }); + }); + + it('adds globalLabels if provided', async () => { + const apmSystem = new ApmSystem({ active: true, globalLabels: { alpha: 'one' } }); + await apmSystem.setup(); + expect(apm.addLabels).toHaveBeenCalledWith({ alpha: 'one' }); + }); + + describe('http request normalization', () => { + let windowSpy: any; + + beforeEach(() => { + windowSpy = jest.spyOn(global as any, 'window', 'get').mockImplementation(() => ({ + location: { + protocol: 'http:', + hostname: 'mykibanadomain.com', + port: '5601', + }, + })); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + it('adds an observe function', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(apm.observe).toHaveBeenCalledWith('transaction:end', expect.any(Function)); + }); + + /** + * Utility function to wrap functions that mutate their input but don't return the mutated value. + * Makes expects easier below. + */ + const returnArg = (func: (input: T) => any): ((input: T) => T) => { + return (input) => { + func(input); + return input; + }; + }; + + it('removes the hostname, port, and protocol only when all match window.location', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + // Strips the hostname, protocol, and port from URLs that are on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /asdf/qwerty' }); + + // Does not modify URLs that are not on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + }); + }); + + it('strips the basePath', async () => { + const apmSystem = new ApmSystem({ active: true }, '/alpha'); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + + // Works with relative URLs as well + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET /alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + }); + }); + }); +}); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 5e4953b96dc5b..3b3c1da01a925 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -17,14 +17,17 @@ * under the License. */ +import type { ApmBase } from '@elastic/apm-rum'; +import { modifyUrl } from '@kbn/std'; +import type { InternalApplicationStart } from './application'; + +/** "GET protocol://hostname:port/pathname" */ +const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OPTIONS|CONNECT|TRACE)\s(.*)$/; + /** * This is the entry point used to boot the frontend when serving a application * that lives in the Kibana Platform. - * - * Any changes to this file should be kept in sync with - * src/legacy/ui/ui_bundles/app_entry_template.js */ -import type { InternalApplicationStart } from './application'; interface ApmConfig { // AgentConfigOptions is not exported from @elastic/apm-rum @@ -42,7 +45,7 @@ export class ApmSystem { * `apmConfig` would be populated with relevant APM RUM agent * configuration if server is started with elastic.apm.* config. */ - constructor(private readonly apmConfig?: ApmConfig) { + constructor(private readonly apmConfig?: ApmConfig, private readonly basePath = '') { this.enabled = apmConfig != null && !!apmConfig.active; } @@ -54,6 +57,8 @@ export class ApmSystem { apm.addLabels(globalLabels); } + this.addHttpRequestNormalization(apm); + init(apmConfig); } @@ -73,4 +78,52 @@ export class ApmSystem { } }); } + + /** + * Adds an observer to the APM configuration for normalizing transactions of the 'http-request' type to remove the + * hostname, protocol, port, and base path. Allows for coorelating data cross different deployments. + */ + private addHttpRequestNormalization(apm: ApmBase) { + apm.observe('transaction:end', (t) => { + if (t.type !== 'http-request') { + return; + } + + /** Split URLs of the from "GET protocol://hostname:port/pathname" into method & hostname */ + const matches = t.name.match(HTTP_REQUEST_TRANSACTION_NAME_REGEX); + if (!matches) { + return; + } + + const [, method, originalUrl] = matches; + // Normalize the URL + const normalizedUrl = modifyUrl(originalUrl, (parts) => { + const isAbsolute = parts.hostname && parts.protocol && parts.port; + // If the request was to a different host, port, or protocol then don't change anything + if ( + isAbsolute && + (parts.hostname !== window.location.hostname || + parts.protocol !== window.location.protocol || + parts.port !== window.location.port) + ) { + return; + } + + // Strip the protocol, hostnname, port, and protocol slashes to normalize + parts.protocol = null; + parts.hostname = null; + parts.port = null; + parts.slashes = false; + + // Replace the basePath if present in the pathname + if (parts.pathname === this.basePath) { + parts.pathname = '/'; + } else if (parts.pathname?.startsWith(`${this.basePath}/`)) { + parts.pathname = parts.pathname?.slice(this.basePath.length); + } + }); + + t.name = `${method} ${normalizedUrl}`; + }); + } } diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index a083196004cf4..4536826a4a267 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -28,7 +28,7 @@ export async function __kbnBootstrap__() { ); let i18nError: Error | undefined; - const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig); + const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig, injectedMetadata.basePath); await Promise.all([ // eslint-disable-next-line no-console diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 2fda8008788e6..1be884f885551 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -412,10 +412,7 @@ export class DashboardPlugin public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; - const { - uiActions, - data: { indexPatterns, search }, - } = plugins; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -447,10 +444,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns, - search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index f3bdfd8e17f0a..bfc52ec33c35c 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -16,11 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; @@ -45,10 +41,9 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - services: SavedObjectKibanaServices + savedObjectStart: SavedObjectsStart ): new (id: string) => SavedObjectDashboard { - const SavedObjectClass = createSavedObjectClass(services); - class SavedDashboard extends SavedObjectClass { + class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type public static type = 'dashboard'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 3bd4d66a693b1..750fec4d4d1f9 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -17,23 +17,19 @@ * under the License. */ -import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; -import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; - indexPatterns: IndexPatternsContract; - search: DataPublicPluginStart['search']; - chrome: ChromeStart; - overlays: OverlayStart; + savedObjects: SavedObjectsStart; } /** * @param services */ -export function createSavedDashboardLoader(services: Services) { - const SavedDashboard = createSavedDashboardClass(services); - return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient); +export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects); + return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.ts b/src/plugins/data/common/es_query/filters/get_filter_params.ts index 2e90ff0fe0691..040bb5b70f7a0 100644 --- a/src/plugins/data/common/es_query/filters/get_filter_params.ts +++ b/src/plugins/data/common/es_query/filters/get_filter_params.ts @@ -26,9 +26,10 @@ export function getFilterParams(filter: Filter) { case FILTERS.PHRASES: return (filter as PhrasesFilter).meta.params; case FILTERS.RANGE: + const { gte, gt, lte, lt } = (filter as RangeFilter).meta.params; return { - from: (filter as RangeFilter).meta.params.gte, - to: (filter as RangeFilter).meta.params.lt, + from: gte ?? gt, + to: lt ?? lte, }; } } diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 153b6a633b66d..2d6637daf4324 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -20,7 +20,6 @@ export * from './constants'; export * from './es_query'; export * from './field_formats'; -export * from './field_mapping'; export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index a8d53223c06d1..6e11bc8f1d508 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -34,34 +34,6 @@ class MockFieldFormatter {} fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; -jest.mock('../../field_mapping', () => { - const originalModule = jest.requireActual('../../field_mapping'); - - return { - ...originalModule, - expandShorthand: jest.fn(() => ({ - id: true, - title: true, - fieldFormatMap: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => []), - }, - fields: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation((fields) => fields), - }, - sourceFilters: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => undefined), - }, - typeMeta: { - _serialize: jest.fn().mockImplementation(() => {}), - _deserialize: jest.fn().mockImplementation(() => undefined), - }, - })), - }; -}); - // helper function to create index patterns function create(id: string) { const { diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 201e9f1ec402c..910c79f5dd0d7 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -400,6 +400,15 @@ export class AggConfig { return this.params.field; } + /** + * Returns the bucket path containing the main value the agg will produce + * (e.g. for sum of bytes it will point to the sum, for median it will point + * to the 50 percentile in the percentile multi value bucket) + */ + getValueBucketPath() { + return this.type.getValueBucketPath(this); + } + makeLabel(percentageMode = false) { if (this.params.customLabel) { return this.params.customLabel; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 1e3839038b0f7..3ffac0c12eb22 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -60,6 +60,7 @@ export interface AggTypeConfig< getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; + getValueBucketPath?: (agg: TAggConfig) => string; } // TODO need to make a more explicit interface for this @@ -210,6 +211,10 @@ export class AggType< return this.params.find((p: TParam) => p.name === name); }; + getValueBucketPath = (agg: TAggConfig) => { + return agg.id; + }; + /** * Generic AggType Constructor * @@ -233,6 +238,10 @@ export class AggType< this.createFilter = config.createFilter; } + if (config.getValueBucketPath) { + this.getValueBucketPath = config.getValueBucketPath; + } + if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index b53ae44c05075..ead88f924731b 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -68,6 +68,7 @@ describe('AggConfig Filters', () => { { gte: 1024, lt: 2048.0, + label: 'A custom label', } ); @@ -78,6 +79,7 @@ describe('AggConfig Filters', () => { expect(filter.range).toHaveProperty('bytes'); expect(filter.range.bytes).toHaveProperty('gte', 1024.0); expect(filter.range.bytes).toHaveProperty('lt', 2048.0); + expect(filter.range.bytes).not.toHaveProperty('label'); expect(filter.meta).toHaveProperty('formattedValue'); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts index 8dea33a450c5d..bea8e577b21fb 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts @@ -25,7 +25,7 @@ import { IBucketAggConfig } from '../bucket_agg_type'; export const createFilterRange = ( getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart'] ) => { - return (aggConfig: IBucketAggConfig, params: any) => { + return (aggConfig: IBucketAggConfig, { label, ...params }: any) => { const { deserialize } = getFieldFormatsStart(); return buildRangeFilter( aggConfig.params.field, diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index 04e64233ce196..8128f1a18a66a 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -69,6 +69,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a string', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); timeBuckets.setInterval('20m'); + const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('20 minutes'); @@ -77,6 +78,23 @@ describe('TimeBuckets', () => { expect(interval.expression).toEqual('20m'); }); + test('getInterval - should scale interval', () => { + const timeBuckets = new TimeBuckets(timeBucketConfig); + const bounds = { + min: moment('2020-03-25'), + max: moment('2020-03-31'), + }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('1m'); + + const interval = timeBuckets.getInterval(); + + expect(interval.description).toEqual('day'); + expect(interval.esValue).toEqual(1); + expect(interval.esUnit).toEqual('d'); + expect(interval.expression).toEqual('1d'); + }); + test('setInterval/getInterval - intreval is a string and bounds is defined', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); const bounds = { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index d054df0c9274e..f11f89317aea6 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -263,18 +263,16 @@ export class TimeBuckets { } const maxLength: number = this._timeBucketConfig['histogram:maxBars']; - const approxLen = Number(duration) / Number(interval); + const minInterval = calcAutoIntervalLessThan(maxLength, Number(duration)); let scaled; - if (approxLen > maxLength) { - scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); + if (interval < minInterval) { + scaled = minInterval; } else { return interval; } - if (+scaled === +interval) return interval; - interval = decorateInterval(interval); return Object.assign(scaled, { preScaled: interval, diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index 169b234845274..bdb6ea7cd4b98 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -41,6 +41,7 @@ export interface AggParamsRange extends BaseAggParams { ranges?: Array<{ from: number; to: number; + label?: string; }>; } @@ -71,7 +72,7 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend key = keys.get(id); if (!key) { - key = new RangeKey(bucket); + key = new RangeKey(bucket, agg.params.ranges); keys.set(id, key); } @@ -102,7 +103,11 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend { from: 1000, to: 2000 }, ], write(aggConfig, output) { - output.params.ranges = aggConfig.params.ranges; + output.params.ranges = (aggConfig.params as AggParamsRange).ranges?.map((range) => ({ + to: range.to, + from: range.from, + })); + output.params.keyed = true; }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/range_key.ts b/src/plugins/data/common/search/aggs/buckets/range_key.ts index cd781f7e082a2..43fdc20e53f55 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_key.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_key.ts @@ -19,14 +19,36 @@ const id = Symbol('id'); +type Ranges = Array< + Partial<{ + from: string | number; + to: string | number; + label: string; + }> +>; + export class RangeKey { [id]: string; gte: string | number; lt: string | number; + label?: string; + + private findCustomLabel( + from: string | number | undefined | null, + to: string | number | undefined | null, + ranges?: Ranges + ) { + return (ranges || []).find( + (range) => + ((from == null && range.from == null) || range.from === from) && + ((to == null && range.to == null) || range.to === to) + )?.label; + } - constructor(bucket: any) { + constructor(bucket: any, allRanges?: Ranges) { this.gte = bucket.from == null ? -Infinity : bucket.from; this.lt = bucket.to == null ? +Infinity : bucket.to; + this.label = this.findCustomLabel(bucket.from, bucket.to, allRanges); this[id] = RangeKey.idBucket(bucket); } diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 2c5be00c8afea..8f645b4712c7f 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -18,6 +18,7 @@ */ import { AggConfigs } from '../agg_configs'; +import { METRIC_TYPES } from '../metrics'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -133,5 +134,49 @@ describe('Terms Agg', () => { expect(params.include).toStrictEqual([1.1, 2, 3.33]); expect(params.exclude).toStrictEqual([4, 5.555, 6]); }); + + test('uses correct bucket path for sorting by median', () => { + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + const field = { + name: 'field', + indexPattern, + }; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: 'test', + params: { + field: { + name: 'string_field', + type: 'string', + }, + orderAgg: { + type: METRIC_TYPES.MEDIAN, + params: { + field: { + name: 'number_field', + type: 'number', + }, + }, + }, + }, + type: BUCKET_TYPES.TERMS, + }, + ], + { typesRegistry: mockAggTypesRegistry() } + ); + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' }); + }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 1363d38748c8b..3d543e6c5f574 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -41,7 +41,6 @@ import { export const termsAggFilter = [ '!top_hits', '!percentiles', - '!median', '!std_dev', '!derivative', '!moving_avg', @@ -198,14 +197,14 @@ export const getTermsBucketAgg = () => return; } - const orderAggId = orderAgg.id; + const orderAggPath = orderAgg.getValueBucketPath(); if (orderAgg.parentId && aggs) { orderAgg = aggs.byId(orderAgg.parentId); } output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAggId] = dir; + order[orderAggPath] = dir; }, }, { diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index f3f2d157ebafc..42298586cb68f 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -63,6 +63,12 @@ describe('AggTypeMetricMedianProvider class', () => { expect(dsl.median.percentiles.percents).toEqual([50]); }); + it('points to right value within multi metric for value bucket path', () => { + expect(aggConfigs.byId(METRIC_TYPES.MEDIAN)!.getValueBucketPath()).toEqual( + `${METRIC_TYPES.MEDIAN}.50` + ); + }); + it('converts the response', () => { const agg = aggConfigs.getResponseAggs()[0]; diff --git a/src/plugins/data/common/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts index 7b48a664b5fb9..a189461020915 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.ts @@ -42,6 +42,9 @@ export const getMedianMetricAgg = () => { values: { field: aggConfig.getFieldDisplayName() }, }); }, + getValueBucketPath(aggConfig) { + return `${aggConfig.id}.50`; + }, params: [ { name: 'field', diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts index 20d8cfc105e49..28646c092c01c 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts @@ -79,6 +79,16 @@ describe('getFormatWithAggs', () => { expect(getFormat).toHaveBeenCalledTimes(1); }); + test('returns custom label for range if provided', () => { + const mapping = { id: 'range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: 1, lt: 20, label: 'custom' })).toBe('custom'); + // underlying formatter is not called because custom label can be used directly + expect(getFormat).toHaveBeenCalledTimes(0); + }); + test('creates custom format for terms', () => { const mapping = { id: 'terms', diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts index 01369206ab3cf..a8134619fec0d 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts @@ -48,6 +48,9 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma const customFormats: Record IFieldFormat> = { range: () => { const RangeFormat = FieldFormat.from((range: any) => { + if (range.label) { + return range.label; + } const nestedFormatter = params as SerializedFieldFormat; const format = getFieldFormat({ id: nestedFormatter.id, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 9d417684b1651..c041511745be2 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -463,8 +463,6 @@ export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; export { ACTION_GLOBAL_APPLY_FILTER, ApplyGlobalFilterActionContext } from './actions'; -export * from '../common/field_mapping'; - /* * Plugin setup */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0477fb68b3c1e..2ed3e440040de 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -128,6 +128,7 @@ export class AggConfig { getTimeRange(): import("../../../public").TimeRange | undefined; // (undocumented) getValue(bucket: any): any; + getValueBucketPath(): string; // (undocumented) id: string; // (undocumented) @@ -651,11 +652,6 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; -// Warning: (ae-forgotten-export) The symbol "ShorthandFieldMapObject" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export const expandShorthand: (sh: Record) => MappingObject; - // Warning: (ae-missing-release-tag) "extractReferences" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -782,16 +778,6 @@ export type FieldFormatsStart = Omit // @public (undocumented) export const fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList; -// @public (undocumented) -export interface FieldMappingSpec { - // (undocumented) - _deserialize?: (mapping: string) => any | undefined; - // (undocumented) - _serialize?: (mapping: any) => string | undefined; - // (undocumented) - type: ES_FIELD_TYPES; -} - // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1518,9 +1504,6 @@ export interface KueryNode { type: keyof NodeTypes; } -// @public (undocumented) -export type MappingObject = Record; - // Warning: (ae-missing-release-tag) "MatchAllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 2d582b30bcd14..734e88e085661 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -127,7 +127,7 @@ export class SearchService implements Plugin { request: SearchStrategyRequest, options: ISearchOptions ) => { - return search(request, options).toPromise() as Promise; + return search(request, options).toPromise(); }, onResponse: handleResponse, legacy: { diff --git a/src/plugins/data/public/ui/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss index 3a9a0df4332c8..efe2e28ac3b8a 100644 --- a/src/plugins/data/public/ui/filter_bar/_variables.scss +++ b/src/plugins/data/public/ui/filter_bar/_variables.scss @@ -1,3 +1,4 @@ $kbnGlobalFilterItemBorderColor: tintOrShade($euiColorMediumShade, 35%, 20%); $kbnGlobalFilterItemBorderColorExcluded: tintOrShade($euiColorDanger, 70%, 50%); $kbnGlobalFilterItemPinnedColorExcluded: tintOrShade($euiColorDanger, 30%, 20%); +$kbnGlobalFilterItemEditorWidth: 420px; // if changing this make sure to also change `FILTER_EDITOR_WIDTH` in ./filter_item.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index fdd952e2207d9..0d544ac9ad16a 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -23,7 +23,7 @@ import classNames from 'classnames'; import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; -import { FilterItem } from './filter_item'; +import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; import { useKibana } from '../../../../kibana_react/public'; import { IIndexPattern } from '../..'; @@ -112,7 +112,7 @@ function FilterBarUI(props: Props) { repositionOnScroll > -
+
{ private renderRegularEditor() { return (
- + {this.renderFieldInput()} {this.renderOperatorInput()} diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index cbff20115f8ea..018f41ab82bfc 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -62,6 +62,13 @@ export type FilterLabelStatus = | typeof FILTER_ITEM_WARNING | typeof FILTER_ITEM_ERROR; +/** + * @remarks + * if changing this make sure to also change + * $kbnGlobalFilterItemEditorWidth + */ +export const FILTER_EDITOR_WIDTH = 420; + export function FilterItem(props: Props) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [indexPatternExists, setIndexPatternExists] = useState(undefined); @@ -228,7 +235,7 @@ export function FilterItem(props: Props) { }, { id: 1, - width: 420, + width: FILTER_EDITOR_WIDTH, content: (
({ DEFAULT_QUERY_LANGUAGE: 'lucene', @@ -29,6 +31,8 @@ jest.mock('../../../common', () => ({ let fetch: ReturnType; let callCluster: LegacyAPICaller; +let collectorFetchContext: CollectorFetchContext; +const collectorFetchContextMock = createCollectorFetchContextMock(); function setupMockCallCluster( optCount: { optInCount?: number; optOutCount?: number } | null, @@ -89,40 +93,64 @@ describe('makeKQLUsageCollector', () => { it('should return opt in data from the .kibana/kql-telemetry doc', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(1); expect(fetchResponse.optOutCount).toBe(0); }); it('should return the default query language set in advanced settings', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('kuery'); }); // Indicates the user has modified the setting at some point but the value is currently the default it('should return the kibana default query language if the config value is null', async () => { setupMockCallCluster({ optInCount: 1 }, null); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('lucene'); }); it('should indicate when the default language has never been modified by the user', async () => { setupMockCallCluster({ optInCount: 1 }, undefined); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); it('should default to 0 opt in counts if the .kibana/kql-telemetry doc does not exist', async () => { setupMockCallCluster(null, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(0); expect(fetchResponse.optOutCount).toBe(0); }); it('should default to the kibana default language if the config document does not exist', async () => { setupMockCallCluster(null, 'missingConfigDoc'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); }); diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 109d6f812334d..21a1843d1ec81 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common'; const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE; @@ -30,7 +30,7 @@ export interface Usage { } export function fetchProvider(index: string) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const [response, config] = await Promise.all([ callCluster('get', { index, diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 3551767eab017..344bc18c7b4b6 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -19,7 +19,8 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { LegacyAPICaller, SharedGlobalConfig } from 'kibana/server'; +import { SharedGlobalConfig } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Usage } from './register'; interface SearchTelemetrySavedObject { @@ -27,7 +28,7 @@ interface SearchTelemetrySavedObject { } export function fetchProvider(config$: Observable) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); const response = await callCluster('search', { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 504ce728481f0..2dbcc3196aa75 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -35,7 +35,8 @@ describe('ES search strategy', () => { }, }, }); - const mockContext = { + + const mockContext = ({ core: { uiSettings: { client: { @@ -44,7 +45,8 @@ describe('ES search strategy', () => { }, elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } }, }, - }; + } as unknown) as RequestHandlerContext; + const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { @@ -57,44 +59,51 @@ describe('ES search strategy', () => { expect(typeof esSearch.search).toBe('function'); }); - it('calls the API caller with the params with defaults', async () => { + it('calls the API caller with the params with defaults', async (done) => { const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - ignore_unavailable: true, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + ignore_unavailable: true, + track_total_hits: true, + }); + done(); + }); }); - it('calls the API caller with overridden defaults', async () => { + it('calls the API caller with overridden defaults', async (done) => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + track_total_hits: true, + }); + done(); + }); }); - it('has all response parameters', async () => { - const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - const response = await esSearch.search((mockContext as unknown) as RequestHandlerContext, { - params, - }); - - expect(response.isRunning).toBe(false); - expect(response.isPartial).toBe(false); - expect(response).toHaveProperty('loaded'); - expect(response).toHaveProperty('rawResponse'); - }); + it('has all response parameters', async (done) => + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search( + { + params: { index: 'logstash-*' }, + }, + {}, + mockContext + ) + .subscribe((data) => { + expect(data.isRunning).toBe(false); + expect(data.isPartial).toBe(false); + expect(data).toHaveProperty('loaded'); + expect(data).toHaveProperty('rawResponse'); + done(); + })); }); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 6e185d30ad56a..92cc941e14853 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { Observable, from } from 'rxjs'; import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; import { toSnakeCase } from './to_snake_case'; @@ -29,6 +29,7 @@ import { getTotalLoaded, getShardTimeout, shimAbortSignal, + IEsSearchResponse, } from '..'; export const esSearchStrategyProvider = ( @@ -37,47 +38,52 @@ export const esSearchStrategyProvider = ( usage?: SearchUsage ): ISearchStrategy => { return { - search: async (context, request, options) => { - logger.debug(`search ${request.params?.index}`); - const config = await config$.pipe(first()).toPromise(); - const uiSettingsClient = await context.core.uiSettings.client; + search: (request, options, context) => + from( + new Promise(async (resolve, reject) => { + logger.debug(`search ${request.params?.index}`); + const config = await config$.pipe(first()).toPromise(); + const uiSettingsClient = await context.core.uiSettings.client; - // Only default index pattern type is supported here. - // See data_enhanced for other type support. - if (!!request.indexType) { - throw new Error(`Unsupported index pattern type ${request.indexType}`); - } + // Only default index pattern type is supported here. + // See data_enhanced for other type support. + if (!!request.indexType) { + throw new Error(`Unsupported index pattern type ${request.indexType}`); + } - // ignoreThrottled is not supported in OSS - const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams(uiSettingsClient); + // ignoreThrottled is not supported in OSS + const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams( + uiSettingsClient + ); - const params = toSnakeCase({ - ...defaultParams, - ...getShardTimeout(config), - ...request.params, - }); + const params = toSnakeCase({ + ...defaultParams, + ...getShardTimeout(config), + ...request.params, + }); - try { - const promise = shimAbortSignal( - context.core.elasticsearch.client.asCurrentUser.search(params), - options?.abortSignal - ); - const { body: rawResponse } = (await promise) as ApiResponse>; + try { + const promise = shimAbortSignal( + context.core.elasticsearch.client.asCurrentUser.search(params), + options?.abortSignal + ); + const { body: rawResponse } = (await promise) as ApiResponse>; - if (usage) usage.trackSuccess(rawResponse.took); + if (usage) usage.trackSuccess(rawResponse.took); - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { - isPartial: false, - isRunning: false, - rawResponse, - ...getTotalLoaded(rawResponse._shards), - }; - } catch (e) { - if (usage) usage.trackError(); - throw e; - } - }, + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + resolve({ + isPartial: false, + isRunning: false, + rawResponse, + ...getTotalLoaded(rawResponse._shards), + }); + } catch (e) { + if (usage) usage.trackError(); + reject(e); + } + }) + ), }; }; diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index d4404c318ab47..834e5de5c3121 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { Observable, from } from 'rxjs'; import { CoreSetup, @@ -66,7 +66,8 @@ describe('Search service', () => { }, }, }; - mockDataStart.search.search.mockResolvedValue(response); + + mockDataStart.search.search.mockReturnValue(from(Promise.resolve(response))); const mockContext = {}; const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; @@ -83,7 +84,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response, @@ -91,12 +92,16 @@ describe('Search service', () => { }); it('handler throws an error if the search throws an error', async () => { - mockDataStart.search.search.mockRejectedValue({ - message: 'oh no', - body: { - error: 'oops', - }, - }); + const rejectedValue = from( + Promise.reject({ + message: 'oh no', + body: { + error: 'oops', + }, + }) + ); + + mockDataStart.search.search.mockReturnValue(rejectedValue); const mockContext = {}; const mockBody = { id: undefined, params: {} }; @@ -114,7 +119,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 492ad4395b32a..1e8433d9685e3 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -49,14 +49,16 @@ export function registerSearchRoute( const [, , selfStart] = await getStartServices(); try { - const response = await selfStart.search.search( - context, - { ...searchRequest, id }, - { - abortSignal, - strategy, - } - ); + const response = await selfStart.search + .search( + { ...searchRequest, id }, + { + abortSignal, + strategy, + }, + context + ) + .toPromise(); return res.ok({ body: { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6e66f8027207b..0130d3aacc91f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -49,10 +49,10 @@ import { IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, - ISearchOptions, SearchSourceDependencies, SearchSourceService, searchSourceRequiredUiSettings, + ISearchOptions, } from '../../common/search'; import { getShardDelayBucketAgg, @@ -151,13 +151,7 @@ export class SearchService implements Plugin { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), getSearchStrategy: this.getSearchStrategy, - search: ( - context: RequestHandlerContext, - searchRequest: IKibanaSearchRequest, - options: Record - ) => { - return this.search(context, searchRequest, options); - }, + search: this.search.bind(this), searchSource: { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); @@ -175,7 +169,13 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: (key: string): T => uiSettingsCache[key], - search: (searchRequest, options) => { + search: < + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse + >( + searchStrategyRequest: SearchStrategyRequest, + options: ISearchOptions + ) => { /** * Unless we want all SearchSource users to provide both a KibanaRequest * (needed for index patterns) AND the RequestHandlerContext (needed for @@ -195,7 +195,12 @@ export class SearchService implements Plugin { }, }, } as RequestHandlerContext; - return this.search(fakeRequestHandlerContext, searchRequest, options); + + return this.search( + searchStrategyRequest, + options, + fakeRequestHandlerContext + ).toPromise(); }, // onResponse isn't used on the server, so we just return the original value onResponse: (req, res) => res, @@ -234,13 +239,15 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - context: RequestHandlerContext, searchRequest: SearchStrategyRequest, - options: ISearchOptions - ): Promise => { - return this.getSearchStrategy( + options: ISearchOptions, + context: RequestHandlerContext + ) => { + const strategy = this.getSearchStrategy( options.strategy || this.defaultSearchStrategyName - ).search(context, searchRequest, options); + ); + + return strategy.search(searchRequest, options, context); }; private getSearchStrategy = < diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 0de4ef529e896..9ba06d88dc4b3 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Observable } from 'rxjs'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ISearchOptions, @@ -57,6 +58,22 @@ export interface ISearchSetup { __enhance: (enhancements: SearchEnhancements) => void; } +/** + * Search strategy interface contains a search method that takes in a request and returns a promise + * that resolves to a response. + */ +export interface ISearchStrategy< + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse +> { + search: ( + request: SearchStrategyRequest, + options: ISearchOptions, + context: RequestHandlerContext + ) => Observable; + cancel?: (context: RequestHandlerContext, id: string) => Promise; +} + export interface ISearchStart< SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse @@ -69,28 +86,8 @@ export interface ISearchStart< getSearchStrategy: ( name: string ) => ISearchStrategy; - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options: ISearchOptions - ) => Promise; + search: ISearchStrategy['search']; searchSource: { asScoped: (request: KibanaRequest) => Promise; }; } - -/** - * Search strategy interface contains a search method that takes in a request and returns a promise - * that resolves to a response. - */ -export interface ISearchStrategy< - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse -> { - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options?: ISearchOptions - ) => Promise; - cancel?: (context: RequestHandlerContext, id: string) => Promise; -} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 07afad1c96a06..f2c8ff5344b9a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -713,7 +713,7 @@ export interface ISearchStart ISearchStrategy; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; + search: ISearchStrategy['search']; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -727,7 +727,7 @@ export interface ISearchStrategy Promise; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; + search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; } // @public (undocumented) @@ -1140,7 +1140,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:91:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 1a23f6deb5fa5..67c93ad8a406c 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -12,13 +12,9 @@ "urlForwarding", "navigation", "uiActions", - "visualizations" + "visualizations", + "savedObjects" ], "optionalPlugins": ["home", "share"], - "requiredBundles": [ - "kibanaUtils", - "home", - "savedObjects", - "kibanaReact" - ] + "requiredBundles": ["kibanaUtils", "home", "kibanaReact"] } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index de1faaf9fc19d..139b2ca69d9e4 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -211,12 +211,7 @@ export function DiscoverLegacy({ /> )} {resultState === 'uninitialized' && } - {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} - -
- -
-
+ {resultState === 'loading' && } {resultState === 'ready' && (
diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx index 4e1754638d479..e3cc396783628 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function LoadingSpinner() { return ( - <> +

@@ -30,6 +30,6 @@ export function LoadingSpinner() { - +

); } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index fdb14b3f1f63e..27844cc2347b9 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -37,7 +37,6 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi import { SharePluginStart } from 'src/plugins/share/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { VisualizationsStart } from 'src/plugins/visualizations/public'; -import { SavedObjectKibanaServices } from 'src/plugins/saved_objects/public'; import { DiscoverStartPlugins } from './plugin'; import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; @@ -78,12 +77,9 @@ export async function buildServices( context: PluginInitializerContext, getEmbeddableInjector: any ): Promise { - const services: SavedObjectKibanaServices = { + const services = { savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }; const savedObjectService = createSavedSearchesLoader(services); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b1bbc89b62d9d..11ec4f08d9514 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -41,7 +41,7 @@ import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwardi import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; @@ -141,6 +141,7 @@ export interface DiscoverStartPlugins { urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; visualizations: VisualizationsStart; + savedObjects: SavedObjectsStart; } const innerAngularName = 'app/discover'; @@ -351,10 +352,7 @@ export class DiscoverPlugin urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }), }; } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 2b8574a8fa118..1ec4549f05d49 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -16,16 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../saved_objects/public'; -export function createSavedSearchClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSearch extends SavedObjectClass { +export function createSavedSearchClass(savedObjects: SavedObjectsStart) { + class SavedSearch extends savedObjects.SavedObjectClass { public static type: string = 'search'; public static mapping = { title: 'text', @@ -70,5 +64,5 @@ export function createSavedSearchClass(services: SavedObjectKibanaServices) { } } - return SavedSearch as new (id: string) => SavedObject; + return (SavedSearch as unknown) as new (id: string) => SavedObject; } diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/saved_searches.ts index 0bc332ed8ec74..fd7a185f7012f 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches.ts @@ -17,12 +17,18 @@ * under the License. */ -import { SavedObjectLoader, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; -export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { - const SavedSearchClass = createSavedSearchClass(services); - const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); +interface Services { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; +} + +export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) { + const SavedSearchClass = createSavedSearchClass(savedObjects); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', diff --git a/src/plugins/expressions/.eslintrc.json b/src/plugins/expressions/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/src/plugins/expressions/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 09fb4fae3f201..e8cf497774569 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -24,12 +24,12 @@ export type ExpressionAstNode = | ExpressionAstFunction | ExpressionAstArgument; -export interface ExpressionAstExpression { +export type ExpressionAstExpression = { type: 'expression'; chain: ExpressionAstFunction[]; -} +}; -export interface ExpressionAstFunction { +export type ExpressionAstFunction = { type: 'function'; function: string; arguments: Record; @@ -38,9 +38,9 @@ export interface ExpressionAstFunction { * Debug information added to each function when expression is executed in *debug mode*. */ debug?: ExpressionAstFunctionDebug; -} +}; -export interface ExpressionAstFunctionDebug { +export type ExpressionAstFunctionDebug = { /** * True if function successfully returned output, false if function threw. */ @@ -83,6 +83,6 @@ export interface ExpressionAstFunctionDebug { * timing starts after the arguments have been resolved. */ duration: number | undefined; -} +}; export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index d4c9b0a25d45b..69140453f486d 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { Executor, ExpressionExecOptions } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; @@ -217,7 +218,27 @@ export class Execution< const fn = getByAlias(this.state.get().functions, fnName); if (!fn) { - return createError({ message: `Function ${fnName} could not be found.` }); + return createError({ + name: 'fn not found', + message: i18n.translate('expressions.execution.functionNotFound', { + defaultMessage: `Function {fnName} could not be found.`, + values: { + fnName, + }, + }), + }); + } + + if (fn.disabled) { + return createError({ + name: 'fn is disabled', + message: i18n.translate('expressions.execution.functionDisabled', { + defaultMessage: `Function {fnName} is disabled.`, + values: { + fnName, + }, + }), + }); } let args: Record = {}; diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 20c5b2dd434b5..79bb4c58ab48d 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -62,7 +62,7 @@ export class ExecutionContract< return { type: 'error', error: { - type: e.type, + name: e.name, message: e.message, stack: e.stack, }, diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 81845401d32e4..a658d3457407c 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -21,7 +21,7 @@ import { Executor } from './executor'; import * as expressionTypes from '../expression_types'; import * as expressionFunctions from '../expression_functions'; import { Execution } from '../execution'; -import { parseExpression } from '../ast'; +import { ExpressionAstFunction, parseExpression } from '../ast'; describe('Executor', () => { test('can instantiate', () => { @@ -152,4 +152,47 @@ describe('Executor', () => { }); }); }); + + describe('.inject', () => { + const executor = new Executor(); + + const injectFn = jest.fn().mockImplementation((args, references) => args); + const extractFn = jest.fn().mockReturnValue({ args: {}, references: [] }); + + const fooFn = { + name: 'foo', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + extract: (state: ExpressionAstFunction['arguments']) => { + return extractFn(state); + }, + inject: (state: ExpressionAstFunction['arguments']) => { + return injectFn(state); + }, + fn: jest.fn(), + }; + executor.registerFunction(fooFn); + + test('calls inject function for every expression function in expression', () => { + executor.inject( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + [] + ); + expect(injectFn).toBeCalledTimes(5); + }); + + describe('.extract', () => { + test('calls extract function for every expression function in expression', () => { + executor.extract( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}') + ); + expect(extractFn).toBeCalledTimes(5); + }); + }); + }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 2b5f9f2556d89..28aae8c8f4834 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -19,6 +19,7 @@ /* eslint-disable max-classes-per-file */ +import { cloneDeep, mapValues } from 'lodash'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; @@ -26,9 +27,12 @@ import { Execution, ExecutionParams } from '../execution/execution'; import { IRegistry } from '../types'; import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; -import { ExpressionAstExpression } from '../ast'; +import { ExpressionAstExpression, ExpressionAstFunction } from '../ast'; import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; +import { getByAlias } from '../util'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; export interface ExpressionExecOptions { /** @@ -83,7 +87,8 @@ export class FunctionsRegistry implements IRegistry { } } -export class Executor = Record> { +export class Executor = Record> + implements PersistableState { static createWithDefaults = Record>( state?: ExecutorState ): Executor { @@ -197,6 +202,56 @@ export class Executor = Record void + ) { + for (const link of ast.chain) { + const { function: fnName, arguments: fnArgs } = link; + const fn = getByAlias(this.state.get().functions, fnName); + + if (fn) { + // if any of arguments are expressions we should migrate those first + link.arguments = mapValues(fnArgs, (asts, argName) => { + return asts.map((arg) => { + if (typeof arg === 'object') { + return this.walkAst(arg, action); + } + return arg; + }); + }); + + action(fn, link); + } + } + + return ast; + } + + public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) { + return this.walkAst(cloneDeep(ast), (fn, link) => { + link.arguments = fn.inject(link.arguments, references); + }); + } + + public extract(ast: ExpressionAstExpression) { + const allReferences: SavedObjectReference[] = []; + const newAst = this.walkAst(cloneDeep(ast), (fn, link) => { + const { state, references } = fn.extract(link.arguments); + link.arguments = state; + allReferences.push(...references); + }); + return { state: newAst, references: allReferences }; + } + + public telemetry(ast: ExpressionAstExpression, telemetryData: Record) { + this.walkAst(cloneDeep(ast), (fn, link) => { + telemetryData = fn.telemetry(link.arguments, telemetryData); + }); + + return telemetryData; + } + public fork(): Executor { const initialState = this.state.get(); const fork = new Executor(initialState); diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 71f0d91510136..0b56d3c169ff4 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -17,12 +17,16 @@ * under the License. */ +import { identity } from 'lodash'; import { AnyExpressionFunctionDefinition } from './types'; import { ExpressionFunctionParameter } from './expression_function_parameter'; import { ExpressionValue } from '../expression_types/types'; import { ExecutionContext } from '../execution'; +import { ExpressionAstFunction } from '../ast'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { /** * Name of function */ @@ -60,8 +64,34 @@ export class ExpressionFunction { */ inputTypes: string[] | undefined; + disabled: boolean; + telemetry: ( + state: ExpressionAstFunction['arguments'], + telemetryData: Record + ) => Record; + extract: ( + state: ExpressionAstFunction['arguments'] + ) => { state: ExpressionAstFunction['arguments']; references: SavedObjectReference[] }; + inject: ( + state: ExpressionAstFunction['arguments'], + references: SavedObjectReference[] + ) => ExpressionAstFunction['arguments']; + constructor(functionDefinition: AnyExpressionFunctionDefinition) { - const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition; + const { + name, + type, + aliases, + fn, + help, + args, + inputTypes, + context, + disabled, + telemetry, + inject, + extract, + } = functionDefinition; this.name = name; this.type = type; @@ -70,6 +100,10 @@ export class ExpressionFunction { Promise.resolve(fn(input, params, handlers as ExecutionContext)); this.help = help || ''; this.inputTypes = inputTypes || context?.types; + this.disabled = disabled || false; + this.telemetry = telemetry || ((s, c) => c); + this.inject = inject || identity; + this.extract = extract || ((s) => ({ state: s, references: [] })); for (const [key, arg] of Object.entries(args || {})) { this.args[key] = new ExpressionFunctionParameter(key, arg); diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index d58d872aff722..caaef541aefd5 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -30,6 +30,8 @@ import { ExpressionFunctionVar, ExpressionFunctionTheme, } from './specs'; +import { ExpressionAstFunction } from '../ast'; +import { PersistableStateDefinition } from '../../../kibana_utils/common'; /** * `ExpressionFunctionDefinition` is the interface plugins have to implement to @@ -41,12 +43,17 @@ export interface ExpressionFunctionDefinition< Arguments extends Record, Output, Context extends ExecutionContext = ExecutionContext -> { +> extends PersistableStateDefinition { /** * The name of the function, as will be used in expression. */ name: Name; + /** + * if set to true function will be disabled (but its migrate function will still be available) + */ + disabled?: boolean; + /** * Name of type of value this function outputs. */ diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index ebaedcbba0d23..7607945d8a664 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -20,20 +20,16 @@ import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { ExpressionValueRender } from './render'; import { getType } from '../get_type'; +import { SerializableState } from '../../../../kibana_utils/common'; +import { ErrorLike } from '../../util'; const name = 'error'; export type ExpressionValueError = ExpressionValueBoxed< 'error', { - error: { - message: string; - type?: string; - name?: string; - stack?: string; - original?: Error; - }; - info?: unknown; + error: ErrorLike; + info?: SerializableState; } >; diff --git a/src/plugins/expressions/common/expression_types/specs/range.ts b/src/plugins/expressions/common/expression_types/specs/range.ts index 3d7170cf715d7..53fd4894fd2be 100644 --- a/src/plugins/expressions/common/expression_types/specs/range.ts +++ b/src/plugins/expressions/common/expression_types/specs/range.ts @@ -26,6 +26,7 @@ export interface Range { type: typeof name; from: number; to: number; + label?: string; } export const range: ExpressionTypeDefinition = { @@ -41,7 +42,7 @@ export const range: ExpressionTypeDefinition = { }, to: { render: (value: Range): ExpressionValueRender<{ text: string }> => { - const text = `from ${value.from} to ${value.to}`; + const text = value?.label || `from ${value.from} to ${value.to}`; return { type: 'render', as: 'text', diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index b5c98fada07c4..4a87fd9e7f331 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -23,6 +23,8 @@ import { ExpressionAstExpression } from '../ast'; import { ExecutionContract } from '../execution/execution_contract'; import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; +import { SavedObjectReference } from '../../../../core/types'; +import { PersistableState } from '../../../kibana_utils/common'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -154,7 +156,7 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService { +export class ExpressionsService implements PersistableState { public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -256,6 +258,36 @@ export class ExpressionsService { return fork; }; + /** + * Extracts telemetry from expression AST + * @param state expression AST to extract references from + */ + public readonly telemetry = ( + state: ExpressionAstExpression, + telemetryData: Record = {} + ) => { + return this.executor.telemetry(state, telemetryData); + }; + + /** + * Extracts saved object references from expression AST + * @param state expression AST to extract references from + * @returns new expression AST with references removed and array of references + */ + public readonly extract = (state: ExpressionAstExpression) => { + return this.executor.extract(state); + }; + + /** + * Injects saved object references into expression AST + * @param state expression AST to update + * @param references array of saved object references + * @returns new expression AST with references injected + */ + public readonly inject = (state: ExpressionAstExpression, references: SavedObjectReference[]) => { + return this.executor.inject(state, references); + }; + /** * Returns Kibana Platform *setup* life-cycle contract. Useful to return the * same contract on server-side and browser-side. diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 9bdab74efd6f9..293afd46d4de5 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -19,9 +19,17 @@ import { ExpressionValueError } from '../../common'; -type ErrorLike = Partial>; +export type SerializedError = { + name: string; + message: string; + stack?: string; +}; -export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({ +export type ErrorLike = SerializedError & { + original?: SerializedError; +}; + +export const createError = (err: string | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -32,6 +40,11 @@ export const createError = (err: string | Error | ErrorLike): ExpressionValueErr : undefined, message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', - original: err instanceof Error ? err : undefined, + original: + err instanceof Error + ? err + : typeof err === 'object' && 'original' in err && err.original instanceof Error + ? err.original + : undefined, }, }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 763147df6d922..c7b6190b96ed7 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -189,10 +189,11 @@ export interface ExecutionState extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } +// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> { +export class Executor = Record> implements PersistableState { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -203,6 +204,11 @@ export class Executor = Record): void; // (undocumented) + extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; + // (undocumented) fork(): Executor; // @deprecated (undocumented) readonly functions: FunctionsRegistry; @@ -214,6 +220,10 @@ export class Executor = Record; + // Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; // (undocumented) registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -221,9 +231,11 @@ export class Executor = Record = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; // (undocumented) readonly state: ExecutorContainer; + // (undocumented) + telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; // @deprecated (undocumented) readonly types: TypesRegistry; -} + } // Warning: (ae-forgotten-export) The symbol "ExecutorPureTransitions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ExecutorPureSelectors" needs to be exported by the entry point index.d.ts @@ -252,12 +264,10 @@ export type ExpressionAstArgument = string | boolean | number | ExpressionAstExp // Warning: (ae-missing-release-tag) "ExpressionAstExpression" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstExpression { - // (undocumented) - chain: ExpressionAstFunction[]; - // (undocumented) +export type ExpressionAstExpression = { type: 'expression'; -} + chain: ExpressionAstFunction[]; +}; // Warning: (ae-missing-release-tag) "ExpressionAstExpressionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -273,16 +283,12 @@ export interface ExpressionAstExpressionBuilder { // Warning: (ae-missing-release-tag) "ExpressionAstFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstFunction { - // (undocumented) +export type ExpressionAstFunction = { + type: 'function'; + function: string; arguments: Record; - // Warning: (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts debug?: ExpressionAstFunctionDebug; - // (undocumented) - function: string; - // (undocumented) - type: 'function'; -} +}; // Warning: (ae-missing-release-tag) "ExpressionAstFunctionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -320,23 +326,35 @@ export interface ExpressionExecutor { // Warning: (ae-missing-release-tag) "ExpressionFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { constructor(functionDefinition: AnyExpressionFunctionDefinition); // (undocumented) accepts: (type: string) => boolean; aliases: string[]; args: Record; + // (undocumented) + disabled: boolean; + // (undocumented) + extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; fn: (input: ExpressionValue, params: Record, handlers: object) => ExpressionValue; help: string; + // (undocumented) + inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; name: string; + // (undocumented) + telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; type: string; } +// Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> { +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition { aliases?: string[]; args: { [key in keyof Arguments]: ArgumentType; @@ -345,6 +363,7 @@ export interface ExpressionFunctionDefinition>; @@ -492,6 +511,8 @@ export class ExpressionRendererRegistry implements IRegistry // // @public (undocumented) export interface ExpressionRenderError extends Error { + // (undocumented) + original?: Error; // (undocumented) type?: string; } @@ -540,13 +561,17 @@ export { ExpressionsPublicPlugin as Plugin } // Warning: (ae-missing-release-tag) "ExpressionsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export class ExpressionsService { +export class ExpressionsService implements PersistableState { // Warning: (ae-forgotten-export) The symbol "ExpressionServiceParams" needs to be exported by the entry point index.d.ts constructor({ executor, renderers, }?: ExpressionServiceParams); // (undocumented) readonly execute: ExpressionsServiceStart['execute']; // (undocumented) readonly executor: Executor; + readonly extract: (state: ExpressionAstExpression) => { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; // (undocumented) readonly fork: () => ExpressionsService; // (undocumented) @@ -558,6 +583,7 @@ export class ExpressionsService { // (undocumented) readonly getType: ExpressionsServiceStart['getType']; readonly getTypes: () => ReturnType; + readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; readonly registerFunction: (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void; // (undocumented) readonly registerRenderer: (definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) => void; @@ -571,6 +597,7 @@ export class ExpressionsService { start(): ExpressionsServiceStart; // (undocumented) stop(): void; + readonly telemetry: (state: ExpressionAstExpression, telemetryData?: Record) => Record; } // Warning: (ae-missing-release-tag) "ExpressionsServiceSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -705,14 +732,8 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1048,6 +1069,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // @@ -1076,7 +1099,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; reload$?: Observable; // (undocumented) - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; } // Warning: (ae-missing-release-tag) "ReactExpressionRendererType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1162,6 +1185,12 @@ export type TypeToString = KnownTypeToString | UnmappedTypeStrings; export type UnmappedTypeStrings = 'date' | 'filter'; +// Warnings were encountered during analysis: +// +// src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 12476c70044b5..99d170c96666d 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -35,7 +35,10 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { className?: string; dataAttrs?: string[]; expression: string | ExpressionAstExpression; - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: ( + message?: string | null, + error?: ExpressionRenderError | null + ) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; onEvent?: (event: ExpressionRendererEvent) => void; /** @@ -186,7 +189,10 @@ export const ReactExpressionRenderer = ({
{state.isEmpty && } {state.isLoading && } - {!state.isLoading && state.error && renderError && renderError(state.error.message)} + {!state.isLoading && + state.error && + renderError && + renderError(state.error.message, state.error)}
extends ExecutorState state: 'not-started' | 'pending' | 'result' | 'error'; } +// Warning: (ae-forgotten-export) The symbol "PersistableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Executor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Executor = Record> { +export class Executor = Record> implements PersistableState { constructor(state?: ExecutorState); // (undocumented) get context(): Record; @@ -185,6 +186,11 @@ export class Executor = Record): void; // (undocumented) + extract(ast: ExpressionAstExpression): { + state: ExpressionAstExpression; + references: SavedObjectReference[]; + }; + // (undocumented) fork(): Executor; // @deprecated (undocumented) readonly functions: FunctionsRegistry; @@ -196,6 +202,10 @@ export class Executor = Record; + // Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + inject(ast: ExpressionAstExpression, references: SavedObjectReference[]): ExpressionAstExpression; // (undocumented) registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -203,9 +213,11 @@ export class Executor = Record = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; // (undocumented) readonly state: ExecutorContainer; + // (undocumented) + telemetry(ast: ExpressionAstExpression, telemetryData: Record): Record; // @deprecated (undocumented) readonly types: TypesRegistry; -} + } // Warning: (ae-forgotten-export) The symbol "ExecutorPureTransitions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ExecutorPureSelectors" needs to be exported by the entry point index.d.ts @@ -234,12 +246,10 @@ export type ExpressionAstArgument = string | boolean | number | ExpressionAstExp // Warning: (ae-missing-release-tag) "ExpressionAstExpression" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstExpression { - // (undocumented) - chain: ExpressionAstFunction[]; - // (undocumented) +export type ExpressionAstExpression = { type: 'expression'; -} + chain: ExpressionAstFunction[]; +}; // Warning: (ae-missing-release-tag) "ExpressionAstExpressionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -255,16 +265,12 @@ export interface ExpressionAstExpressionBuilder { // Warning: (ae-missing-release-tag) "ExpressionAstFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExpressionAstFunction { - // (undocumented) +export type ExpressionAstFunction = { + type: 'function'; + function: string; arguments: Record; - // Warning: (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts debug?: ExpressionAstFunctionDebug; - // (undocumented) - function: string; - // (undocumented) - type: 'function'; -} +}; // Warning: (ae-missing-release-tag) "ExpressionAstFunctionBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -292,23 +298,35 @@ export type ExpressionAstNode = ExpressionAstExpression | ExpressionAstFunction // Warning: (ae-missing-release-tag) "ExpressionFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class ExpressionFunction { +export class ExpressionFunction implements PersistableState { constructor(functionDefinition: AnyExpressionFunctionDefinition); // (undocumented) accepts: (type: string) => boolean; aliases: string[]; args: Record; + // (undocumented) + disabled: boolean; + // (undocumented) + extract: (state: ExpressionAstFunction['arguments']) => { + state: ExpressionAstFunction['arguments']; + references: SavedObjectReference[]; + }; fn: (input: ExpressionValue, params: Record, handlers: object) => ExpressionValue; help: string; + // (undocumented) + inject: (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; name: string; + // (undocumented) + telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; type: string; } +// Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> { +export interface ExpressionFunctionDefinition, Output, Context extends ExecutionContext = ExecutionContext> extends PersistableStateDefinition { aliases?: string[]; args: { [key in keyof Arguments]: ArgumentType; @@ -317,6 +335,7 @@ export interface ExpressionFunctionDefinition>; @@ -565,14 +584,8 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -881,6 +894,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // @@ -966,6 +981,12 @@ export type TypeToString = KnownTypeToString | UnmappedTypeStrings; export type UnmappedTypeStrings = 'date' | 'filter'; +// Warnings were encountered during analysis: +// +// src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts +// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts index 736e79015af9f..54fed3db1de4d 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.test.ts @@ -17,39 +17,39 @@ * under the License. */ -import sinon from 'sinon'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { fetchProvider } from './collector_fetch'; -describe('Sample Data Fetch', () => { - let callClusterMock: sinon.SinonStub; +const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; - beforeEach(() => { - callClusterMock = sinon.stub(); - }); +describe('Sample Data Fetch', () => { + let collectorFetchContext: CollectorFetchContext; test('uninitialized .kibana', async () => { const fetch = fetchProvider('index'); - const telemetry = await fetch(callClusterMock); + collectorFetchContext = getMockFetchClients(); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(`undefined`); }); test('installed data set', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -67,27 +67,23 @@ Object { test('multiple installed data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -106,17 +102,13 @@ Object { test('installed data set, missing counts', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -132,34 +124,30 @@ Object { test('installed and uninstalled data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test0', - _source: { - updated_at: '2019-03-13T22:29:32Z', - 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, - }, - }, - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test0', + _source: { + updated_at: '2019-03-13T22:29:32Z', + 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, + }, + }, + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts index d43458cfc64db..7df9b14d2efb1 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts @@ -19,6 +19,7 @@ import { get } from 'lodash'; import moment from 'moment'; +import { CollectorFetchContext } from '../../../../../usage_collection/server'; interface SearchHit { _id: string; @@ -41,7 +42,7 @@ export interface TelemetryResponse { } export function fetchProvider(index: string) { - return async (callCluster: any) => { + return async ({ callCluster }: CollectorFetchContext) => { const response = await callCluster('search', { index, body: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 23a77c2d4c288..c1457c64080a6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -17,16 +17,13 @@ * under the License. */ -import { - savedObjectsRepositoryMock, - loggingSystemMock, - elasticsearchServiceMock, -} from '../../../../../core/server/mocks'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL, @@ -53,8 +50,7 @@ describe('telemetry_application_usage', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) @@ -67,7 +63,7 @@ describe('telemetry_application_usage', () => { test('if no savedObjectClient initialised, return undefined', async () => { expect(collector.isReady()).toBe(false); - expect(await collector.fetch(callCluster, esClient)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); jest.runTimersToTime(ROLL_INDICES_START); }); @@ -85,7 +81,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -142,7 +138,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: total + 1 + 10, clicks_7_days: total + 1, @@ -202,7 +198,7 @@ describe('telemetry_application_usage', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: 1, clicks_7_days: 0, diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts index b712e9ebbce48..e8efa9997c459 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts @@ -21,7 +21,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerCoreUsageCollector } from '.'; import { coreUsageDataServiceMock } from '../../../../../core/server/mocks'; import { CoreUsageData } from 'src/core/server/'; @@ -35,7 +35,7 @@ describe('telemetry_core', () => { return createUsageCollectionSetupMock().makeUsageCollector(config); }); - const callCluster = jest.fn().mockImplementation(() => ({})); + const collectorFetchContext = createCollectorFetchContextMock(); const coreUsageDataStart = coreUsageDataServiceMock.createStartContract(); const getCoreUsageDataReturnValue = (Symbol('core telemetry') as any) as CoreUsageData; coreUsageDataStart.getCoreUsageData.mockResolvedValue(getCoreUsageDataReturnValue); @@ -48,6 +48,6 @@ describe('telemetry_core', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toEqual(getCoreUsageDataReturnValue); + expect(await collector.fetch(collectorFetchContext)).toEqual(getCoreUsageDataReturnValue); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts index 465b21e3578ba..03184d7385861 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts @@ -20,10 +20,12 @@ import { CspConfig, ICspConfig } from '../../../../../core/server'; import { createCspCollector } from './csp_collector'; import { httpServiceMock } from '../../../../../core/server/mocks'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; describe('csp collector', () => { let httpMock: ReturnType; - const mockCallCluster = null as any; + // changed for consistency with expected implementation + const mockedFetchContext = createCollectorFetchContextMock(); function updateCsp(config: Partial) { httpMock.csp = new CspConfig(config); @@ -36,28 +38,28 @@ describe('csp collector', () => { test('fetches whether strict mode is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(true); updateCsp({ strict: false }); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(false); }); test('fetches whether the legacy browser warning is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(true); updateCsp({ warnLegacyBrowsers: false }); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(false); }); test('fetches whether the csp rules have been changed or not', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(false); updateCsp({ rules: ['not', 'default'] }); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(true); }); test('does not include raw csp rules under any property names', async () => { @@ -69,7 +71,7 @@ describe('csp collector', () => { // // We use a snapshot here to ensure csp.rules isn't finding its way into the // payload under some new and unexpected variable name (e.g. cspRules). - expect(await collector.fetch(mockCallCluster)).toMatchInlineSnapshot(` + expect(await collector.fetch(mockedFetchContext)).toMatchInlineSnapshot(` Object { "rulesChangedFromDefault": false, "strict": true, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts index 2bfe59d7dd4fc..88ccb2016d420 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts @@ -22,7 +22,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerKibanaUsageCollector } from './'; describe('telemetry_kibana', () => { @@ -35,7 +35,12 @@ describe('telemetry_kibana', () => { }); const legacyConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; - const callCluster = jest.fn().mockImplementation(() => ({})); + + const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; + }; beforeAll(() => registerKibanaUsageCollector(usageCollectionMock, legacyConfig$)); afterAll(() => jest.clearAllTimers()); @@ -46,7 +51,7 @@ describe('telemetry_kibana', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(getMockFetchClients())).toStrictEqual({ index: '.kibana-tests', dashboard: { total: 0 }, visualization: { total: 0 }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts index 5b56e1a9b596f..d292b2d5ace0e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts @@ -44,7 +44,7 @@ export function getKibanaUsageCollector( graph_workspace: { total: { type: 'long' } }, timelion_sheet: { total: { type: 'long' } }, }, - async fetch(callCluster) { + async fetch({ callCluster }) { const { kibana: { index }, } = await legacyConfig$.pipe(take(1)).toPromise(); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts index d4b635448d0a3..e671f739ee083 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts @@ -21,6 +21,7 @@ import { uiSettingsServiceMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerManagementUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_application_usage_collector', () => { const uiSettingsClient = uiSettingsServiceMock.createClient(); const getUiSettingsClient = jest.fn(() => uiSettingsClient); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => { registerManagementUsageCollector(usageCollectionMock, getUiSettingsClient); @@ -59,11 +60,11 @@ describe('telemetry_application_usage_collector', () => { uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({ 'my-key': { userValue: 'my-value' }, })); - await expect(collector.fetch(callCluster)).resolves.toMatchSnapshot(); + await expect(collector.fetch(mockedFetchContext)).resolves.toMatchSnapshot(); }); test('fetch() should not fail if invoked when not ready', async () => { getUiSettingsClient.mockImplementationOnce(() => undefined as any); - await expect(collector.fetch(callCluster)).resolves.toBe(undefined); + await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts index a527d4d03c6fc..61990730812cc 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts @@ -21,6 +21,7 @@ import { Subject } from 'rxjs'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerOpsStatsCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ops_stats', () => { }); const metrics$ = new Subject(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); const metric: OpsMetrics = { collected_at: new Date('2020-01-01 01:00:00'), @@ -92,7 +93,7 @@ describe('telemetry_ops_stats', () => { test('should return something when there is a metric', async () => { metrics$.next(metric); expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster)).toMatchSnapshot({ + expect(await collector.fetch(mockedFetchContext)).toMatchSnapshot({ concurrent_connections: 20, os: { load: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts index d6f40a2a6867f..48e4e0d99d3cd 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts @@ -21,6 +21,7 @@ import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerUiMetricUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ui_metric', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerUiMetricUsageCollector(usageCollectionMock, registerType, getUsageCollector) @@ -47,7 +48,7 @@ describe('telemetry_ui_metric', () => { }); test('if no savedObjectClient initialised, return undefined', async () => { - expect(await collector.fetch(callCluster)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); }); test('when savedObjectClient is initialised, return something', async () => { @@ -61,7 +62,7 @@ describe('telemetry_ui_metric', () => { ); getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -85,7 +86,7 @@ describe('telemetry_ui_metric', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ testAppName: [ { key: 'testKeyName1', value: 3 }, { key: 'testKeyName2', value: 5 }, diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 9140de316605c..ecf6aa0569bf7 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -30,7 +30,6 @@ export { export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder'; export { SavedObjectLoader, - createSavedObjectClass, checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal, diff --git a/src/plugins/saved_objects/public/mocks.ts b/src/plugins/saved_objects/public/mocks.ts new file mode 100644 index 0000000000000..d34a6ded7c8de --- /dev/null +++ b/src/plugins/saved_objects/public/mocks.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsStart } from './plugin'; + +const createStartContract = (): SavedObjectsStart => { + return { + SavedObjectClass: jest.fn(), + settings: { + getPerPage: () => 20, + getListingLimit: () => 100, + }, + }; +}; + +export const savedObjectsPluginMock = { + createStartContract, +}; diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index d430c8896484d..0c50180e13d86 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -23,9 +23,10 @@ import './index.scss'; import { createSavedObjectClass } from './saved_object'; import { DataPublicPluginStart } from '../../data/public'; import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; +import { SavedObject } from './types'; export interface SavedObjectsStart { - SavedObjectClass: any; + SavedObjectClass: new (raw: Record) => SavedObject; settings: { getPerPage: () => number; getListingLimit: () => number; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 47390c7dc9104..04fa3647de4c7 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -23,8 +23,8 @@ import { IndexPattern, injectSearchSourceReferences, parseSearchSourceJSON, - expandShorthand, } from '../../../../data/public'; +import { expandShorthand } from './field_mapping'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given diff --git a/src/plugins/data/common/field_mapping/index.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts similarity index 100% rename from src/plugins/data/common/field_mapping/index.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts diff --git a/src/plugins/data/common/field_mapping/mapping_setup.test.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts similarity index 96% rename from src/plugins/data/common/field_mapping/mapping_setup.test.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts index e57699e879a87..9353ff6796b5f 100644 --- a/src/plugins/data/common/field_mapping/mapping_setup.test.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts @@ -18,7 +18,7 @@ */ import { expandShorthand } from './mapping_setup'; -import { ES_FIELD_TYPES } from '../../../data/common'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; describe('mapping_setup', () => { it('allows shortcuts for field types by just setting the value to the type name', () => { diff --git a/src/plugins/data/common/field_mapping/mapping_setup.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts similarity index 96% rename from src/plugins/data/common/field_mapping/mapping_setup.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts index 0bad47d9889f0..804f03d345a96 100644 --- a/src/plugins/data/common/field_mapping/mapping_setup.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts @@ -21,7 +21,7 @@ import { mapValues, isString } from 'lodash'; import { FieldMappingSpec, MappingObject } from './types'; // import from ./common/types to prevent circular dependency of kibana_utils <-> data plugin -import { ES_FIELD_TYPES } from '../../../data/common/types'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; /** @private */ type ShorthandFieldMapObject = FieldMappingSpec | ES_FIELD_TYPES | 'json'; diff --git a/src/plugins/data/common/field_mapping/types.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts similarity index 94% rename from src/plugins/data/common/field_mapping/types.ts rename to src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts index 973a58d3baec4..a60a6b10623fc 100644 --- a/src/plugins/data/common/field_mapping/types.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ES_FIELD_TYPES } from '../../../data/common'; +import { ES_FIELD_TYPES } from '../../../../../data/public'; /** @public */ export interface FieldMappingSpec { diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index 24e467ad18ac4..a0ab527ce1743 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -18,7 +18,8 @@ */ import _ from 'lodash'; import { SavedObject, SavedObjectConfig } from '../../types'; -import { extractSearchSourceReferences, expandShorthand } from '../../../../data/public'; +import { extractSearchSourceReferences } from '../../../../data/public'; +import { expandShorthand } from './field_mapping'; export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { // mapping definition for the fields that this object will expose diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx index f2eeedb5b7372..fff266bf964b0 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx @@ -32,7 +32,7 @@ import React, { useState } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', { - defaultMessage: 'Please secure your installation', + defaultMessage: 'Your data is not secure', }); export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountPoint = ( @@ -47,7 +47,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP @@ -66,7 +66,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP size="s" color="primary" fill - href="https://www.elastic.co/what-is/elastic-stack-security" + href="https://www.elastic.co/what-is/elastic-stack-security?blade=kibanasecuritymessage" target="_blank" > {i18n.translate('security.checkup.learnMoreButtonText', { diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index d8c709d867a3f..3134cc265fba1 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -6,7 +6,6 @@ "requiredBundles": [ "kibanaLegacy", "kibanaUtils", - "savedObjects", "visTypeTimelion" ], "requiredPlugins": [ @@ -14,6 +13,7 @@ "data", "navigation", "visTypeTimelion", + "savedObjects", "kibanaLegacy" ] } diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts index a4963ee6b1b03..e0425ac94c59b 100644 --- a/src/plugins/timelion/public/application.ts +++ b/src/plugins/timelion/public/application.ts @@ -41,7 +41,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../kibana_legacy/public'; -import { TimelionPluginDependencies } from './plugin'; +import { TimelionPluginStartDependencies } from './plugin'; import { DataPublicPluginStart } from '../../data/public'; // @ts-ignore import { initTimelionApp } from './app'; @@ -50,7 +50,7 @@ export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; mountParams: AppMountParameters; core: CoreStart; - plugins: TimelionPluginDependencies; + plugins: TimelionPluginStartDependencies; timelionPanels: Map; } @@ -137,7 +137,7 @@ function createLocalIconModule() { .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) { +function createLocalTopNavModule(navigation: TimelionPluginStartDependencies['navigation']) { angular .module('app/timelion/TopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 7656a808dfb00..b435cc6fd399b 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -36,20 +36,29 @@ import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { VisualizationsStart } from '../../visualizations/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; import { VisTypeTimelionPluginStart, VisTypeTimelionPluginSetup, } from '../../vis_type_timelion/public'; -export interface TimelionPluginDependencies { +export interface TimelionPluginSetupDependencies { + data: DataPublicPluginSetup; + visTypeTimelion: VisTypeTimelionPluginSetup; +} + +export interface TimelionPluginStartDependencies { data: DataPublicPluginStart; navigation: NavigationPublicPluginStart; visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; + savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ -export class TimelionPlugin implements Plugin { +export class TimelionPlugin + implements Plugin { initializerContext: PluginInitializerContext; private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; @@ -60,7 +69,7 @@ export class TimelionPlugin implements Plugin { } public setup( - core: CoreSetup, + core: CoreSetup, { data, visTypeTimelion, @@ -122,7 +131,7 @@ export class TimelionPlugin implements Plugin { pluginInitializerContext: this.initializerContext, timelionPanels, core: coreStart, - plugins: pluginsStart as TimelionPluginDependencies, + plugins: pluginsStart, }); return () => { unlistenParentHistory(); diff --git a/src/plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts index 0958cce860126..3fe66fabebe73 100644 --- a/src/plugins/timelion/public/services/_saved_sheet.ts +++ b/src/plugins/timelion/public/services/_saved_sheet.ts @@ -18,16 +18,11 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { createSavedObjectClass, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsStart } from '../../../saved_objects/public'; // Used only by the savedSheets service, usually no reason to change this -export function createSavedSheetClass( - services: SavedObjectKibanaServices, - config: IUiSettingsClient -) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSheet extends SavedObjectClass { +export function createSavedSheetClass(savedObjects: SavedObjectsStart, config: IUiSettingsClient) { + class SavedSheet extends savedObjects.SavedObjectClass { static type = 'timelion-sheet'; // if type:sheet has no mapping, we push this mapping into ES diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts index 9ad529cb0134b..4c360ad558234 100644 --- a/src/plugins/timelion/public/services/saved_sheets.ts +++ b/src/plugins/timelion/public/services/saved_sheets.ts @@ -23,15 +23,7 @@ import { RenderDeps } from '../application'; export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) { const savedObjectsClient = deps.core.savedObjects.client; - const services = { - savedObjectsClient, - indexPatterns: deps.plugins.data.indexPatterns, - search: deps.plugins.data.search, - chrome: deps.core.chrome, - overlays: deps.core.overlays, - }; - - const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings); + const SavedSheet = createSavedSheetClass(deps.plugins.savedObjects, deps.core.uiSettings); const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient); savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index aae633a956c48..1d706843b8b4e 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -37,10 +37,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t ``` 3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. - ```ts // server/collectors/register.ts - import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { APICluster } from 'kibana/server'; interface Usage { @@ -63,7 +62,7 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, - fetch: async (callCluster: APICluster, esClient: IClusterClient) => { + fetch: async (collectorFetchContext: CollectorFetchContext) => { // query ES and get some data // summarize the data into a model diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 8491bdb0c957c..6608849ae8b99 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -45,11 +45,23 @@ export type MakeSchemaFrom = { : RecursiveMakeSchemaFrom[Key]>; }; +export interface CollectorFetchContext { + /** + * @depricated Scoped Legacy Elasticsearch client: use esClient instead + */ + callCluster: LegacyAPICaller; + /** + * Request-scoped Elasticsearch client: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + */ + esClient: ElasticsearchClient; +} export interface CollectorOptions { type: string; init?: Function; schema?: MakeSchemaFrom; - fetch: (callCluster: LegacyAPICaller, esClient?: ElasticsearchClient) => Promise | T; + fetch: (collectorFetchContext: CollectorFetchContext) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed * data model for internal bulk upload. See defaultFormatterForBulkUpload for diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 3f943ad8bf2ff..c8114269f30e4 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -81,7 +81,9 @@ describe('CollectorSet', () => { collectors.registerCollector( new Collector(logger, { type: 'MY_TEST_COLLECTOR', - fetch: (caller: any) => caller(), + fetch: (collectorFetchContext: any) => { + return collectorFetchContext.callCluster(); + }, isReady: () => true, }) ); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 7bf4e19c72cc0..88204c61bfd8d 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -122,9 +122,6 @@ export class CollectorSet { return allReady; }; - // all collections eventually pass through bulkFetch. - // the shape of the response is different when using the new ES client as is the error handling. - // We'll handle the refactor for using the new client in a follow up PR. public bulkFetch = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, @@ -136,7 +133,7 @@ export class CollectorSet { try { return { type: collector.type, - result: await collector.fetch(callCluster, esClient), // each collector must ensure they handle the response appropriately. + result: await collector.fetch({ callCluster, esClient }), }; } catch (err) { this.logger.warn(err); diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 1816e845b4d66..c294ba77d3cdb 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -24,5 +24,6 @@ export { SchemaField, MakeSchemaFrom, CollectorOptions, + CollectorFetchContext, } from './collector'; export { UsageCollector } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 87761bca9a507..80e34b1502cda 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -26,6 +26,7 @@ export { SchemaField, CollectorOptions, Collector, + CollectorFetchContext, } from './collector'; export { UsageCollectionSetup } from './plugin'; export { config } from './config'; diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index e1f13304165a1..d08db1eaec0e1 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -20,6 +20,7 @@ import { loggingSystemMock } from '../../../core/server/mocks'; import { UsageCollectionSetup } from './plugin'; import { CollectorSet } from './collector'; +export { createCollectorFetchContextMock } from './usage_collection.mock'; const createSetupContract = () => { return { diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index 7a6d16d6950ec..4e5e84200d65e 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -17,8 +17,10 @@ * under the License. */ +import { elasticsearchServiceMock } from '../../../../src/core/server/mocks'; + import { CollectorOptions } from './collector/collector'; -import { UsageCollectionSetup } from './index'; +import { UsageCollectionSetup, CollectorFetchContext } from './index'; export { CollectorOptions }; @@ -45,3 +47,11 @@ export const createUsageCollectionSetupMock = () => { usageCollectionSetupMock.areAllCollectorsReady.mockResolvedValue(true); return usageCollectionSetupMock; }; + +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { + callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + }; + return collectorFetchClientsMock; +} diff --git a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx index 4c843791153b0..f1497631b66c5 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx @@ -106,7 +106,7 @@ describe('OrderAggParamEditor component', () => { mount(); - expect(setValue).toHaveBeenCalledWith('agg5'); + expect(setValue).toHaveBeenCalledWith('agg3'); }); it('defaults to the _key metric if no agg is compatible', () => { diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 0efd6e7746fd2..707b14c23ea75 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,12 +22,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EventEmitter } from 'events'; import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; -import { EditorRenderProps } from 'src/plugins/visualize/public'; +import { EditorRenderProps, IEditorController } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; const DefaultEditor = lazy(() => import('./default_editor')); -class DefaultEditorController { +class DefaultEditorController implements IEditorController { constructor( private el: HTMLElement, private vis: Vis, diff --git a/src/plugins/vis_type_timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts index ea08310499a96..242be515e52bc 100644 --- a/src/plugins/vis_type_timelion/server/routes/validate_es.ts +++ b/src/plugins/vis_type_timelion/server/routes/validate_es.ts @@ -57,10 +57,17 @@ export function validateEsRoute(router: IRouter, core: CoreSetup) { let resp; try { - resp = await deps.data.search.search(context, body, { - strategy: ES_SEARCH_STRATEGY, - }); - resp = resp.rawResponse; + resp = ( + await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + context + ) + .toPromise() + ).rawResponse; } catch (errResp) { resp = errResp; } diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index c5fc4b7b93269..8be3cf5171c65 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import es from './index'; - import tlConfigFn from '../fixtures/tl_config'; import * as aggResponse from './lib/agg_response_to_series_list'; import buildRequest from './lib/build_request'; @@ -36,7 +36,10 @@ function stubRequestAndServer(response, indexPatternSavedObjects = []) { getStartServices: sinon .stub() .returns( - Promise.resolve([{}, { data: { search: { search: () => Promise.resolve(response) } } }]) + Promise.resolve([ + {}, + { data: { search: { search: () => from(Promise.resolve(response)) } } }, + ]) ), savedObjectsClient: { find: function () { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js index bfa8d75900d11..fc3250f0d4726 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js @@ -132,9 +132,15 @@ export default new Datasource('es', { const deps = (await tlConfig.getStartServices())[1]; - const resp = await deps.data.search.search(tlConfig.context, body, { - strategy: ES_SEARCH_STRATEGY, - }); + const resp = await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + tlConfig.context + ) + .toPromise(); if (!resp.rawResponse._shards.total) { throw new Error( diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 34f339ce24c21..0f64c570088d7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -21,6 +21,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import { createTickFormatter } from './tick_formatter'; +import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => { @@ -63,15 +64,7 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig * If not, return a formatted value from elasticsearch */ if (row.labelFormatted) { - const momemntObj = moment(row.labelFormatted); - let val; - - if (momemntObj.isValid()) { - val = momemntObj.format(dateFormat); - } else { - val = row.labelFormatted; - } - + const val = labelDateFormatter(row.labelFormatted, dateFormat); set(variables, `${_.snakeCase(row.label)}.formatted`, val); } }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts new file mode 100644 index 0000000000000..c4a0f10c5748c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { labelDateFormatter } from './label_date_formatter'; + +const dateString = '2020-09-24T18:59:02.000Z'; + +describe('Label Date Formatter Function', () => { + it('Should format the date string', () => { + const label = labelDateFormatter(dateString); + expect(label).toEqual(moment(dateString).format('lll')); + }); + + it('Should format the date string on the given formatter', () => { + const label = labelDateFormatter(dateString, 'MM/DD/YYYY'); + expect(label).toEqual(moment(dateString).format('MM/DD/YYYY')); + }); + + it('Returns the label if it is not date string', () => { + const label = labelDateFormatter('test date'); + expect(label).toEqual('test date'); + }); + + it('Returns the label if it is a number string', () => { + const label = labelDateFormatter('1'); + expect(label).toEqual('1'); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts new file mode 100644 index 0000000000000..f4de19b084c7c --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +export const labelDateFormatter = (label: string, dateformat = 'lll') => { + let formattedLabel = label; + // Use moment isValid function on strict mode + const isDate = moment(label, '', true).isValid(); + if (isDate) { + formattedLabel = moment(label).format(dateformat); + } + return formattedLabel; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 8b63d1b5043f5..ccf486bff5626 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -19,6 +19,7 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; +import { labelDateFormatter } from './lib/label_date_formatter'; import { last, findIndex, first } from 'lodash'; import { calculateLabel } from '../../../../../plugins/vis_type_timeseries/common/calculate_label'; @@ -41,6 +42,7 @@ export function visWithSplits(WrappedComponent) { acc[splitId] = { series: [], label: series.label.toString(), + labelFormatted: series.labelFormatted, }; } @@ -67,7 +69,11 @@ export function visWithSplits(WrappedComponent) { const rows = Object.keys(splitsVisData).map((key) => { const splitData = splitsVisData[key]; - const { series, label } = splitData; + const { series, label, labelFormatted } = splitData; + let additionalLabel = label; + if (labelFormatted) { + additionalLabel = labelDateFormatter(labelFormatted); + } const newSeries = indexOfNonSplit != null && indexOfNonSplit > 0 ? [...series, nonSplitSeries] @@ -84,7 +90,7 @@ export function visWithSplits(WrappedComponent) { model={model} visData={newVisData} onBrush={props.onBrush} - additionalLabel={label} + additionalLabel={additionalLabel} backgroundColor={props.backgroundColor} getConfig={props.getConfig} /> diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 664751bbc0ec0..278d7906dde94 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -20,6 +20,7 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { labelDateFormatter } from '../../../components/lib/label_date_formatter'; import { Axis, @@ -165,6 +166,7 @@ export const TimeSeries = ({ { id, label, + labelFormatted, bars, lines, data, @@ -188,14 +190,17 @@ export const TimeSeries = ({ const key = `${id}-${label}`; // Only use color mapping if there is no color from the server const finalColor = color ?? colors.mappedColors.mapping[label]; - + let seriesName = label.toString(); + if (labelFormatted) { + seriesName = labelDateFormatter(labelFormatted); + } if (bars?.show) { return ( - {item.label} + {item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 4dcc67dc46976..ceae784cf74a6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import { AbstractSearchStrategy } from './abstract_search_strategy'; describe('AbstractSearchStrategy', () => { @@ -55,7 +56,7 @@ describe('AbstractSearchStrategy', () => { test('should return response', async () => { const searches = [{ body: 'body', index: 'index' }]; - const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); + const searchFn = jest.fn().mockReturnValue(from(Promise.resolve({}))); const responses = await abstractSearchStrategy.search( { @@ -82,7 +83,6 @@ describe('AbstractSearchStrategy', () => { expect(responses).toEqual([{}]); expect(searchFn).toHaveBeenCalledWith( - {}, { params: { body: 'body', @@ -92,7 +92,8 @@ describe('AbstractSearchStrategy', () => { }, { strategy: 'es', - } + }, + {} ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 2eb92b2b777e8..7b62ad310a354 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -60,20 +60,22 @@ export class AbstractSearchStrategy { const requests: any[] = []; bodies.forEach((body) => { requests.push( - deps.data.search.search( - req.requestContext, - { - params: { - ...body, - ...this.additionalParams, + deps.data.search + .search( + { + params: { + ...body, + ...this.additionalParams, + }, + indexType: this.indexType, }, - indexType: this.indexType, - }, - { - ...options, - strategy: this.searchStrategyName, - } - ) + { + ...options, + strategy: this.searchStrategyName, + }, + req.requestContext + ) + .toPromise() ); }); return Promise.all(requests); diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index a5f095a4c4f3d..0969174c7143c 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { tsvbTelemetrySavedObjectType } from '../saved_objects'; @@ -49,7 +49,7 @@ export class ValidationTelemetryService implements Plugin({ type: 'tsvb-validation', isReady: () => this.kibanaIndex !== '', - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async ({ callCluster }) => { try { const response = await callCluster('get', { index: this.kibanaIndex, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index 891ebf6582670..6f17703bc9dee 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { LegacyAPICaller } from 'src/core/server'; import { getStats } from './get_usage_collector'; import { HomeServerPluginSetup } from '../../../home/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; const mockedSavedObjects = [ // vega-lite lib spec @@ -70,8 +70,11 @@ const mockedSavedObjects = [ }, ]; -const getMockCallCluster = (hits?: unknown[]) => - jest.fn().mockReturnValue(Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; +const getMockCollectorFetchContext = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; describe('Vega visualization usage collector', () => { const mockIndex = 'mock_index'; @@ -101,19 +104,23 @@ describe('Vega visualization usage collector', () => { }; test('Returns undefined when no results found (undefined)', async () => { - const result = await getStats(getMockCallCluster(), mockIndex, mockDeps); + const result = await getStats(getMockCollectorFetchContext().callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await getStats(getMockCallCluster([]), mockIndex, mockDeps); + const result = await getStats( + getMockCollectorFetchContext([]).callCluster, + mockIndex, + mockDeps + ); expect(result).toBeUndefined(); }); test('Returns undefined when no vega saved objects found', async () => { - const mockCallCluster = getMockCallCluster([ + const mockCollectorFetchContext = getMockCollectorFetchContext([ { _id: 'visualization:myvis-123', _source: { @@ -122,13 +129,13 @@ describe('Vega visualization usage collector', () => { }, }, ]); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Should ingnore sample data visualizations', async () => { - const mockCallCluster = getMockCallCluster([ + const mockCollectorFetchContext = getMockCollectorFetchContext([ { _id: 'visualization:sampledata-123', _source: { @@ -146,14 +153,14 @@ describe('Vega visualization usage collector', () => { }, ]); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { - const mockCallCluster = getMockCallCluster(mockedSavedObjects); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toMatchObject({ vega_lib_specs_total: 2, diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index 433b786ed46a2..e092fc8acfd71 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -20,6 +20,7 @@ import { of } from 'rxjs'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; @@ -59,10 +60,14 @@ describe('registerVegaUsageCollector', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; - const mockCallCluster = jest.fn(); - const fetchResult = await usageCollectorConfig.fetch(mockCallCluster); + const mockedCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex, mockDeps); + expect(mockGetStats).toBeCalledWith( + mockedCollectorFetchContext.callCluster, + mockIndex, + mockDeps + ); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts index af62821f7cdc0..e4772dad99d40 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts @@ -35,7 +35,7 @@ export function registerVegaUsageCollector( vega_lite_lib_specs_total: { type: 'long' }, vega_use_map_total: { type: 'long' }, }, - fetch: async (callCluster) => { + fetch: async ({ callCluster }) => { const { index } = (await config.pipe(first()).toPromise()).kibana; return await getStats(callCluster, index, dependencies); diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index 688987b1104a1..0ced74e2733d3 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,7 +3,14 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector" ], + "requiredPlugins": [ + "data", + "expressions", + "uiActions", + "embeddable", + "inspector", + "savedObjects" + ], "optionalPlugins": ["usageCollection"], - "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] + "requiredBundles": ["kibanaUtils", "discover"] } diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 99c13b42b8b28..081399fd1fbea 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -36,7 +36,7 @@ export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; -export { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; +export type { VisTypeAlias, VisType, BaseVisTypeOptions, ReactVisTypeOptions } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 90e4936a58b45..f20e87dbd3b6a 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -28,6 +28,7 @@ import { usageCollectionPluginMock } from '../../../plugins/usage_collection/pub import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; +import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -73,6 +74,7 @@ const createInstance = async () => { dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, + savedObjects: savedObjectsPluginMock.createStartContract(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index be7629ef41145..c1dbe39def64c 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -78,6 +78,7 @@ import { } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; import { DashboardStart } from '../../dashboard/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -113,6 +114,7 @@ export interface VisualizationsStartDeps { application: ApplicationStart; dashboard: DashboardStart; getAttributeService: EmbeddableStart['getAttributeService']; + savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; } @@ -160,7 +162,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard, savedObjects }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); @@ -182,18 +184,13 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, visualizationTypes: types, }); setSavedVisualizationsLoader(savedVisualizationsLoader); const savedSearchLoader = createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, }); setSavedSearchLoader(savedSearchLoader); return { diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index 8edf494ddc0ec..59359fb00cc9f 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -24,17 +24,20 @@ * * NOTE: It's a type of SavedObject, but specific to visualizations. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern } from '../../../../plugins/data/public'; +import { IIndexPattern, IndexPatternsContract } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../discover/public'; +import { SavedObjectsClientContract } from '../../../../core/public'; + +export interface SavedVisServices { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; + indexPatterns: IndexPatternsContract; +} export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; @@ -73,11 +76,10 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; }; -export function createSavedVisClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); +export function createSavedVisClass(services: SavedVisServices) { const savedSearch = createSavedSearchesLoader(services); - class SavedVis extends SavedObjectClass { + class SavedVis extends services.savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { title: 'text', @@ -130,5 +132,5 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) { } } - return SavedVis as new (opts: Record | string) => SavedObject; + return (SavedVis as unknown) as new (opts: Record | string) => SavedObject; } diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts index 0ec3c0dab2e97..760bf3cc7a362 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts @@ -16,19 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { - SavedObjectLoader, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { findListItems } from './find_list_items'; -import { createSavedVisClass } from './_saved_vis'; +import { createSavedVisClass, SavedVisServices } from './_saved_vis'; import { TypesStart } from '../vis_types'; -export interface SavedObjectKibanaServicesWithVisualizations extends SavedObjectKibanaServices { +export interface SavedVisServicesWithVisualizations extends SavedVisServices { visualizationTypes: TypesStart; } export type SavedVisualizationsLoader = ReturnType; -export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisualizations) { +export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; class SavedObjectLoaderVisualize extends SavedObjectLoader { diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 22561decabea4..a46b257c9905c 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,6 +18,6 @@ */ export * from './types_service'; -export { VisType } from './types'; +export type { VisType } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 0cf345bf07be6..7206e9612f102 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -20,6 +20,7 @@ import { IconType } from '@elastic/eui'; import React from 'react'; import { Adapters } from 'src/plugins/inspector'; +import { VisEditorConstructor } from 'src/plugins/visualize/public'; import { ISchemas } from 'src/plugins/vis_default_editor/public'; import { TriggerContextMapping } from '../../../ui_actions/public'; import { Vis, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; @@ -69,12 +70,14 @@ export interface VisType { readonly options: VisTypeOptions; - // TODO: The following types still need to be refined properly. - /** * The editor that should be used to edit visualizations of this type. + * If this is not specified the default visualize editor will be used (and should be configured via schemas) + * and editorConfig. */ - readonly editor?: any; + readonly editor?: VisEditorConstructor; + + // TODO: The following types still need to be refined properly. readonly editorConfig: Record; readonly visConfig: Record; } diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts index 38d88dd65001b..7789e3de13e5a 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts @@ -20,6 +20,7 @@ import { of } from 'rxjs'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerVisualizationsCollector } from './register_visualizations_collector'; @@ -58,10 +59,10 @@ describe('registerVisualizationsCollector', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVisualizationsCollector(mockCollectorSet, mockConfig); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; - const mockCallCluster = jest.fn(); - const fetchResult = await usageCollectorConfig.fetch(mockCallCluster); + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex); + expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.callCluster, mockIndex); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts index 5919b3d20642f..4188f564ed5fd 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts @@ -41,7 +41,7 @@ export function registerVisualizationsCollector( saved_90_days_total: { type: 'long' }, }, }, - fetch: async (callCluster) => { + fetch: async ({ callCluster }) => { const index = (await config.pipe(first()).toPromise()).kibana.index; return await getStats(callCluster, index); }, diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 00fa6e74f952a..6cc8f5c26584a 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -45,6 +45,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; export type PureVisState = SavedVisState; @@ -131,7 +132,14 @@ export interface ByValueVisInstance { export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; +export type VisEditorConstructor = new ( + element: HTMLElement, + vis: Vis, + eventEmitter: EventEmitter, + embeddableHandler: VisualizeEmbeddableContract +) => IEditorController; + export interface IEditorController { - render(props: EditorRenderProps): void; + render(props: EditorRenderProps): Promise | void; destroy(): void; } diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index c5cfa5a4c639b..6010c4f8b163e 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -35,7 +35,12 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices; + const { + data, + createVisEmbeddableFromObject, + savedObjects, + savedObjectsPublic, + } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,10 +60,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( if (vis.data.savedSearchId) { savedSearch = await createSavedSearchesLoader({ savedObjectsClient: savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome, - overlays, + savedObjects: savedObjectsPublic, }).get(vis.data.savedSearchId); } diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index ce0f5fe965d7d..3f9676a9c9385 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -116,6 +116,7 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); @@ -129,10 +130,12 @@ describe('useSavedVisInstance', () => { }); test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { - const { unmount, waitForNextUpdate } = renderHook(() => + const { result, unmount, waitForNextUpdate } = renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) ); + result.current.visEditorRef.current = document.createElement('div'); + await waitForNextUpdate(); unmount(); @@ -158,6 +161,8 @@ describe('useSavedVisInstance', () => { useSavedVisInstance(mockServices, eventEmitter, true, undefined) ); + result.current.visEditorRef.current = document.createElement('div'); + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, { indexPattern: '1a2b3c4d', type: 'area', diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index ec815b8cfcbee..44fbcce82f458 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -44,7 +44,7 @@ export const useSavedVisInstance = ( savedVisInstance?: SavedVisInstance; visEditorController?: IEditorController; }>({}); - const visEditorRef = useRef(null); + const visEditorRef = useRef(null); const visId = useRef(''); useEffect(() => { @@ -102,16 +102,18 @@ export const useSavedVisInstance = ( let visEditorController; // do not create editor in embeded mode - if (isChromeVisible) { - const Editor = vis.type.editor || DefaultEditorController; - visEditorController = new Editor( - visEditorRef.current, - vis, - eventEmitter, - embeddableHandler - ); - } else if (visEditorRef.current) { - embeddableHandler.render(visEditorRef.current); + if (visEditorRef.current) { + if (isChromeVisible) { + const Editor = vis.type.editor || DefaultEditorController; + visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + } else { + embeddableHandler.render(visEditorRef.current); + } } setState({ diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index f2758d0cc01a4..e0286a63b9feb 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -41,7 +41,7 @@ export const useVisByValue = ( useEffect(() => { const { chrome } = services; const getVisInstance = async () => { - if (!valueInput || loaded.current) { + if (!valueInput || loaded.current || !visEditorRef.current) { return; } const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index d437cadad9fab..246806f300800 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -20,7 +20,11 @@ import { PluginInitializerContext } from 'kibana/public'; import { VisualizePlugin } from './plugin'; -export { EditorRenderProps } from './application/types'; +export type { + EditorRenderProps, + IEditorController, + VisEditorConstructor, +} from './application/types'; export { VisualizeConstants } from './application/visualize_constants'; export const plugin = (context: PluginInitializerContext) => { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts new file mode 100644 index 0000000000000..1f703c64bbde3 --- /dev/null +++ b/test/accessibility/apps/kibana_overview.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana overview', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('kibanaOverview'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.removeSampleDataSet('flights'); + await esArchiver.unload('empty_kibana'); + }); + + it('Getting started view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Overview view', async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('kibanaOverview'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 9068a7e06defc..9730eae1e1360 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -36,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/console'), require.resolve('./apps/home'), require.resolve('./apps/filter_panel'), + require.resolve('./apps/kibana_overview'), ], pageObjects, services, diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index c95211e98cdba..1a1631b9db48b 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -32,7 +32,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover field visualize button', () => { + describe('discover field visualize button', function () { + // unskipped on cloud as these tests test the navigation + // from Discover to Visualize which happens only on OSS + this.tags(['skipCloud']); before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index e522f41952a49..60532c81493f9 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -27,10 +27,10 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider const { header } = getPageObjects(['header']); const browser = getService('browser'); const globalNav = getService('globalNav'); - const config = getService('config'); - const defaultFindTimeout = config.get('timeouts.find'); const elasticChart = getService('elasticChart'); const docTable = getService('docTable'); + const config = getService('config'); + const defaultFindTimeout = config.get('timeouts.find'); class DiscoverPage { public async getChartTimespan() { @@ -84,8 +84,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async waitUntilSearchingHasFinished() { - const spinner = await testSubjects.find('loadingSpinner'); - await find.waitForElementHidden(spinner, defaultFindTimeout * 10); + await testSubjects.missingOrFail('loadingSpinner', { timeout: defaultFindTimeout * 10 }); } public async getColumnHeaders() { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 98ab1babd60fe..de895918efbba 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -124,9 +124,10 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await comboBox.set('filterOperatorList', operator); const params = await testSubjects.find('filterParams'); const paramsComboBoxes = await params.findAllByCssSelector( - '[data-test-subj~="filterParamsComboBox"]' + '[data-test-subj~="filterParamsComboBox"]', + 1000 ); - const paramFields = await params.findAllByTagName('input'); + const paramFields = await params.findAllByTagName('input', 1000); for (let i = 0; i < values.length; i++) { let fieldValues = values[i]; if (!Array.isArray(fieldValues)) { diff --git a/x-pack/package.json b/x-pack/package.json index 941ebab2f3d65..484a64fdc2628 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -150,7 +150,6 @@ "base64url": "^3.0.1", "brace": "0.11.1", "broadcast-channel": "^3.0.3", - "canvas": "^2.6.1", "chalk": "^4.1.0", "chance": "1.0.18", "cheerio": "0.22.0", @@ -199,7 +198,7 @@ "loader-utils": "^1.2.3", "lz-string": "^1.4.4", "madge": "3.4.4", - "mapbox-gl": "^1.10.0", + "mapbox-gl": "^1.12.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts deleted file mode 100644 index b20018fcc26f7..0000000000000 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ /dev/null @@ -1,4567 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import uuid from 'uuid'; -import { schema } from '@kbn/config-schema'; -import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client'; -import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; -import { nodeTypes } from '../../../../src/plugins/data/common'; -import { esKuery } from '../../../../src/plugins/data/server'; -import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; -import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; -import { TaskStatus } from '../../task_manager/server'; -import { IntervalSchedule, RawAlert } from './types'; -import { resolvable } from './test_utils'; -import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; -import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; -import { ActionsAuthorization } from '../../actions/server'; -import { eventLogClientMock } from '../../event_log/server/mocks'; -import { QueryEventsBySavedObjectResult } from '../../event_log/server'; -import { SavedObject } from 'kibana/server'; -import { EventsFactory } from './lib/alert_instance_summary_from_event_log.test'; - -const taskManager = taskManagerMock.start(); -const alertTypeRegistry = alertTypeRegistryMock.create(); -const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -const eventLogClient = eventLogClientMock.create(); - -const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); -const actionsAuthorization = actionsAuthorizationMock.create(); - -const kibanaVersion = 'v7.10.0'; -const alertsClientParams: jest.Mocked = { - taskManager, - alertTypeRegistry, - unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, - actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, - spaceId: 'default', - namespace: 'default', - getUserName: jest.fn(), - createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), - logger: loggingSystemMock.create().get(), - encryptedSavedObjectsClient: encryptedSavedObjects, - getActionsClient: jest.fn(), - getEventLogClient: jest.fn(), - kibanaVersion, -}; - -beforeEach(() => { - jest.resetAllMocks(); - alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); - alertsClientParams.getUserName.mockResolvedValue('elastic'); - taskManager.runNow.mockResolvedValue({ id: '' }); - const actionsClient = actionsClientMock.create(); - actionsClient.getBulk.mockResolvedValueOnce([ - { - id: '1', - isPreconfigured: false, - actionTypeId: 'test', - name: 'test', - config: { - foo: 'bar', - }, - }, - { - id: '2', - isPreconfigured: false, - actionTypeId: 'test2', - name: 'test2', - config: { - foo: 'bar', - }, - }, - { - id: 'testPreconfigured', - actionTypeId: '.slack', - isPreconfigured: true, - name: 'test', - }, - ]); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); - - alertTypeRegistry.get.mockImplementation((id) => ({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - })); - alertsClientParams.getEventLogClient.mockResolvedValue(eventLogClient); -}); - -const mockedDateString = '2019-02-12T21:01:22.479Z'; -const mockedDate = new Date(mockedDateString); -const DateOriginal = Date; - -// A version of date that responds to `new Date(null|undefined)` and `Date.now()` -// by returning a fixed date, otherwise should be same as Date. -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -(global as any).Date = class Date { - constructor(...args: unknown[]) { - // sometimes the ctor has no args, sometimes has a single `null` arg - if (args[0] == null) { - // @ts-ignore - return mockedDate; - } else { - // @ts-ignore - return new DateOriginal(...args); - } - } - static now() { - return mockedDate.getTime(); - } - static parse(string: string) { - return DateOriginal.parse(string); - } -}; - -function getMockData(overwrites: Record = {}): CreateOptions['data'] { - return { - enabled: true, - name: 'abc', - tags: ['foo'], - alertTypeId: '123', - consumer: 'bar', - schedule: { interval: '10s' }, - throttle: null, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - ...overwrites, - }; -} - -describe('create()', () => { - let alertsClient: AlertsClient; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - describe('authorization', () => { - function tryToExecuteOperation(options: CreateOptions): Promise { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - - return alertsClient.create(options); - } - - test('ensures user is authorised to create this type of alert under the consumer', async () => { - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); - - await tryToExecuteOperation({ data }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); - }); - - test('throws when user is not authorised to create this type of alert', async () => { - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); - - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to create a "myType" alert for "myApp"`) - ); - - await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to create a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); - }); - }); - - test('creates an alert', async () => { - const data = getMockData(); - const createdAttributes = { - ...data, - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - createdBy: 'elastic', - updatedBy: 'elastic', - muteAll: false, - mutedInstanceIds: [], - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }; - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: createdAttributes, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - ...createdAttributes, - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - const result = await alertsClient.create({ data }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "consumer": "bar", - "createdAt": 2019-02-12T21:01:22.479Z, - "createdBy": "elastic", - "enabled": true, - "id": "1", - "muteAll": false, - "mutedInstanceIds": Array [], - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedAt": 2019-02-12T21:01:22.479Z, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "bar", - "createdAt": "2019-02-12T21:01:22.479Z", - "createdBy": "elastic", - "enabled": true, - "executionStatus": Object { - "error": null, - "lastExecutionDate": "2019-02-12T21:01:22.479Z", - "status": "pending", - }, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "muteAll": false, - "mutedInstanceIds": Array [], - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); - expect(taskManager.schedule).toHaveBeenCalledTimes(1); - expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "alertId": "1", - "spaceId": "default", - }, - "scope": Array [ - "alerting", - ], - "state": Object { - "alertInstances": Object {}, - "alertTypeState": Object {}, - "previousStartedAt": null, - }, - "taskType": "alerting:123", - }, - ] - `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "scheduledTaskId": "task-123", - } - `); - }); - - test('creates an alert with multiple actions', async () => { - const data = getMockData({ - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [], - }); - const result = await alertsClient.create({ data }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test2", - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - }); - - test('creates a disabled alert', async () => { - const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - alertTypeId: '123', - schedule: { interval: 10000 }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.create({ data }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": false, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": 10000, - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(taskManager.schedule).toHaveBeenCalledTimes(0); - }); - - test('should trim alert name when creating API key', async () => { - const data = getMockData({ name: ' my alert name ' }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - name: ' my alert name ', - alertTypeId: '123', - schedule: { interval: 10000 }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - - await alertsClient.create({ data }); - expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); - }); - - test('should validate params', async () => { - const data = getMockData(); - alertTypeRegistry.get.mockReturnValue({ - id: '123', - name: 'Test', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - validate: { - params: schema.object({ - param1: schema.string(), - threshold: schema.number({ min: 0, max: 1 }), - }), - }, - async executor() {}, - producer: 'alerts', - }); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"params invalid: [param1]: expected value of type [string] but got [undefined]"` - ); - }); - - test('throws error if loading actions fails', async () => { - const data = getMockData(); - const actionsClient = actionsClientMock.create(); - actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test Error"` - ); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error and invalidates API key when create saved object fails', async () => { - const data = getMockData(); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test failure"` - ); - expect(taskManager.schedule).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('attempts to remove saved object if scheduling failed', async () => { - const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); - unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test failure"` - ); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test('returns task manager error if cleanup fails, logs to console', async () => { - const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); - unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( - new Error('Saved object delete error') - ); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Task manager error"` - ); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to cleanup alert "1" after scheduling task failed. Error: Saved object delete error' - ); - }); - - test('throws an error if alert type not registerd', async () => { - const data = getMockData(); - alertTypeRegistry.get.mockImplementation(() => { - throw new Error('Invalid type'); - }); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid type"` - ); - }); - - test('calls the API key function', async () => { - const data = getMockData(); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - await alertsClient.create({ data }); - - expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( - 'alert', - { - actions: [ - { - actionRef: 'action_0', - group: 'default', - actionTypeId: 'test', - params: { foo: true }, - }, - ], - alertTypeId: '123', - consumer: 'bar', - name: 'abc', - params: { bar: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - apiKeyOwner: 'elastic', - createdBy: 'elastic', - createdAt: '2019-02-12T21:01:22.479Z', - updatedBy: 'elastic', - enabled: true, - meta: { - versionApiKeyLastmodified: 'v7.10.0', - }, - schedule: { interval: '10s' }, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - tags: ['foo'], - executionStatus: { - lastExecutionDate: '2019-02-12T21:01:22.479Z', - status: 'pending', - error: null, - }, - }, - { - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - } - ); - }); - - test(`doesn't create API key for disabled alerts`, async () => { - const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - await alertsClient.create({ data }); - - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( - 'alert', - { - actions: [ - { - actionRef: 'action_0', - group: 'default', - actionTypeId: 'test', - params: { foo: true }, - }, - ], - alertTypeId: '123', - consumer: 'bar', - name: 'abc', - params: { bar: true }, - apiKey: null, - apiKeyOwner: null, - createdBy: 'elastic', - createdAt: '2019-02-12T21:01:22.479Z', - updatedBy: 'elastic', - enabled: false, - meta: { - versionApiKeyLastmodified: 'v7.10.0', - }, - schedule: { interval: '10s' }, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - tags: ['foo'], - executionStatus: { - lastExecutionDate: '2019-02-12T21:01:22.479Z', - status: 'pending', - error: null, - }, - }, - { - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - } - ); - }); -}); - -describe('enable()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - alertsClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - - describe('authorization', () => { - beforeEach(() => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - alertsClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - - test('ensures user is authorised to enable this type of alert under the consumer', async () => { - await alertsClient.enable({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to enable this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to enable a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to enable a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); - }); - }); - - test('enables an alert', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - updatedBy: 'elastic', - apiKey: null, - apiKeyOwner: null, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.schedule).toHaveBeenCalledWith({ - taskType: `alerting:myType`, - params: { - alertId: '1', - spaceId: 'default', - }, - state: { - alertInstances: {}, - alertTypeState: {}, - previousStartedAt: null, - }, - scope: ['alerting'], - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - scheduledTaskId: 'task-123', - }); - }); - - test('invalidates API key if ever one existed prior to updating', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test(`doesn't enable already enabled alerts`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('sets API key when createAPIKey returns one', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - apiKey: Buffer.from('123:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - }); - - test('falls back when failing to getDecryptedAsInternalUser', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'enable(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws error when failing to load the saved object using SOC', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to get"` - ); - expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error when failing to update the first time', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.update.mockReset(); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to update"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error when failing to update the second time', async () => { - unsecuredSavedObjectsClient.update.mockReset(); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - }, - }); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce( - new Error('Fail to update second time') - ); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to update second time"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(taskManager.schedule).toHaveBeenCalled(); - }); - - test('throws error when failing to schedule task', async () => { - taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to schedule"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - }); -}); - -describe('disable()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: true, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - version: '123', - references: [], - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - }); - - describe('authorization', () => { - test('ensures user is authorised to disable this type of alert under the consumer', async () => { - await alertsClient.disable({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - - test('throws when user is not authorised to disable this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to disable a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to disable a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - }); - - test('disables an alert', async () => { - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('falls back when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test(`doesn't disable already disabled alerts`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - actions: [], - enabled: false, - }, - }); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.remove).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test(`doesn't invalidate when no API key is used`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); - - await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when failing to load decrypted saved object', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(taskManager.remove).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'disable(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws when unsecuredSavedObjectsClient update fails', async () => { - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); - - await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update"` - ); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - test('throws when failing to remove task from task manager', async () => { - taskManager.remove.mockRejectedValueOnce(new Error('Failed to remove task')); - - await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to remove task"` - ); - }); -}); - -describe('muteAll()', () => { - test('mutes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - muteAll: false, - }, - references: [], - version: '123', - }); - - await alertsClient.muteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - muteAll: true, - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, - enabled: false, - scheduledTaskId: null, - updatedBy: 'elastic', - muteAll: false, - }, - references: [], - }); - }); - - test('ensures user is authorised to muteAll this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.muteAll({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to muteAll this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - }); - }); -}); - -describe('unmuteAll()', () => { - test('unmutes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - muteAll: true, - }, - references: [], - version: '123', - }); - - await alertsClient.unmuteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - muteAll: false, - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, - enabled: false, - scheduledTaskId: null, - updatedBy: 'elastic', - muteAll: false, - }, - references: [], - }); - }); - - test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.unmuteAll({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to unmuteAll this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); - }); - }); -}); - -describe('muteInstance()', () => { - test('mutes an alert instance', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - version: '123', - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - mutedInstanceIds: ['2'], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - test('skips muting when alert instance already muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - test('skips muting when alert is muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - muteAll: true, - }, - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - version: '123', - references: [], - }); - }); - - test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - - test('throws when user is not authorised to muteInstance this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - }); -}); - -describe('unmuteInstance()', () => { - test('unmutes an alert instance', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - version: '123', - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { version: '123' } - ); - }); - - test('skips unmuting when alert instance not muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - test('skips unmuting when alert is muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - muteAll: true, - }, - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - version: '123', - references: [], - }); - }); - - test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - - test('throws when user is not authorised to unmuteInstance this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - }); -}); - -describe('get()', () => { - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.get({ id: '1' }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test(`throws an error when references aren't found`, async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [], - }); - await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Action reference \\"action_0\\" not found in alert id: 1"` - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - }); - - test('ensures user is authorised to get this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.get({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('throws when user is not authorised to get this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to get a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to get a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - }); -}); - -describe('getAlertState()', () => { - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: '1', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - - await alertsClient.getAlertState({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test('gets the underlying task from TaskManager', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - - const scheduledTaskId = 'task-123'; - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - enabled: true, - scheduledTaskId, - mutedInstanceIds: [], - muteAll: true, - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: scheduledTaskId, - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: { - alertId: '1', - }, - ownerId: null, - }); - - await alertsClient.getAlertState({ id: '1' }); - expect(taskManager.get).toHaveBeenCalledTimes(1); - expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: '1', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - }); - - test('ensures user is authorised to get this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.getAlertState({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); - }); - - test('throws when user is not authorised to getAlertState this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - // `get` check - authorization.ensureAuthorized.mockResolvedValueOnce(); - // `getAlertState` check - authorization.ensureAuthorized.mockRejectedValueOnce( - new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); - }); - }); -}); - -const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { - page: 1, - per_page: 10000, - total: 0, - data: [], -}; - -const AlertInstanceSummaryIntervalSeconds = 1; - -const BaseAlertInstanceSummarySavedObject: SavedObject = { - id: '1', - type: 'alert', - attributes: { - enabled: true, - name: 'alert-name', - tags: ['tag-1', 'tag-2'], - alertTypeId: '123', - consumer: 'alert-consumer', - schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: mockedDateString, - apiKey: null, - apiKeyOwner: null, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: '2020-08-20T19:23:38Z', - error: null, - }, - }, - references: [], -}; - -function getAlertInstanceSummarySavedObject( - attributes: Partial = {} -): SavedObject { - return { - ...BaseAlertInstanceSummarySavedObject, - attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, - }; -} - -describe('getAlertInstanceSummary()', () => { - let alertsClient: AlertsClient; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - test('runs as expected with some event log data', async () => { - const alertSO = getAlertInstanceSummarySavedObject({ - mutedInstanceIds: ['instance-muted-no-activity'], - }); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); - - const eventsFactory = new EventsFactory(mockedDateString); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-currently-active') - .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .addActiveInstance('instance-previously-active') - .advanceTime(10000) - .addExecute() - .addResolvedInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .getEvents(); - const eventsResult = { - ...AlertInstanceSummaryFindEventsResult, - total: events.length, - data: events, - }; - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); - - const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); - - const result = await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - expect(result).toMatchInlineSnapshot(` - Object { - "alertTypeId": "123", - "consumer": "alert-consumer", - "enabled": true, - "errorMessages": Array [], - "id": "1", - "instances": Object { - "instance-currently-active": Object { - "activeStartDate": "2019-02-12T21:01:22.479Z", - "muted": false, - "status": "Active", - }, - "instance-muted-no-activity": Object { - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - "instance-previously-active": Object { - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2019-02-12T21:01:32.479Z", - "muteAll": false, - "name": "alert-name", - "status": "Active", - "statusEndDate": "2019-02-12T21:01:22.479Z", - "statusStartDate": "2019-02-12T21:00:22.479Z", - "tags": Array [ - "tag-1", - "tag-2", - ], - "throttle": null, - } - `); - }); - - // Further tests don't check the result of `getAlertInstanceSummary()`, as the result - // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself - // has a complete set of tests. These tests just make sure the data gets - // sent into `getAlertInstanceSummary()` as appropriate. - - test('calls saved objects and event log client with default params', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - await alertsClient.getAlertInstanceSummary({ id: '1' }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - Object { - "end": "2019-02-12T21:01:22.479Z", - "page": 1, - "per_page": 10000, - "sort_order": "desc", - "start": "2019-02-12T21:00:22.479Z", - }, - ] - `); - // calculate the expected start/end date for one test - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - expect(end).toBe(mockedDateString); - - const startMillis = Date.parse(start!); - const endMillis = Date.parse(end!); - const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; - expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); - expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); - }); - - test('calls event log client with start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = new Date( - Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 - ).toISOString(); - await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T21:00:22.479Z", - } - `); - }); - - test('calls event log client with relative start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = '2m'; - await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T20:59:22.479Z", - } - `); - }); - - test('invalid start date throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = 'ain"t no way this will get parsed as a date'; - expect( - alertsClient.getAlertInstanceSummary({ id: '1', dateStart }) - ).rejects.toMatchInlineSnapshot( - `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` - ); - }); - - test('saved object get throws an error', async () => { - unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - expect(alertsClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: OMG!]` - ); - }); - - test('findEvents throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); - - // error eaten but logged - await alertsClient.getAlertInstanceSummary({ id: '1' }); - }); -}); - -describe('find()', () => { - const listedTypes = new Set([ - { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }, - ]); - beforeEach(() => { - authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized() {}, - logSuccessfulAuthorization() {}, - }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 1, - per_page: 10, - page: 1, - saved_objects: [ - { - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - score: 1, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }, - ], - }); - alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( - new Set([ - { - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: { - myApp: { read: true, all: true }, - }, - }, - ]) - ); - }); - - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - const result = await alertsClient.find({ options: {} }); - expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "fields": undefined, - "filter": undefined, - "type": "alert", - }, - ] - `); - }); - - describe('authorization', () => { - test('ensures user is query filter types down to those the user is authorized to find', async () => { - const filter = esKuery.fromKueryExpression( - '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' - ); - authorization.getFindAuthorizationFilter.mockResolvedValue({ - filter, - ensureAlertTypeIsAuthorized() {}, - logSuccessfulAuthorization() {}, - }); - - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.find({ options: { filter: 'someTerm' } }); - - const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; - expect(options.filter).toEqual( - nodeTypes.function.buildNode('and', [esKuery.fromKueryExpression('someTerm'), filter]) - ); - expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); - }); - - test('throws if user is not authorized to find any types', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized')); - await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot( - `"not authorized"` - ); - }); - - test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { - const ensureAlertTypeIsAuthorized = jest.fn(); - const logSuccessfulAuthorization = jest.fn(); - authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized, - logSuccessfulAuthorization, - }); - - unsecuredSavedObjectsClient.find.mockReset(); - unsecuredSavedObjectsClient.find.mockResolvedValue({ - total: 1, - per_page: 10, - page: 1, - saved_objects: [ - { - id: '1', - type: 'alert', - attributes: { - actions: [], - alertTypeId: 'myType', - consumer: 'myApp', - tags: ['myTag'], - }, - score: 1, - references: [], - }, - ], - }); - - const alertsClient = new AlertsClient(alertsClientParams); - expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [], - "id": "1", - "schedule": undefined, - "tags": Array [ - "myTag", - ], - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ - fields: ['tags', 'alertTypeId', 'consumer'], - type: 'alert', - }); - expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); - expect(logSuccessfulAuthorization).toHaveBeenCalled(); - }); - }); -}); - -describe('delete()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionTypeId: '.no-op', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.delete.mockResolvedValue({ - success: true, - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - }); - - test('successfully removes an alert', async () => { - const result = await alertsClient.delete({ id: '1' }); - expect(result).toEqual({ success: true }); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - }); - - test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - const result = await alertsClient.delete({ id: '1' }); - expect(result).toEqual({ success: true }); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'delete(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test(`doesn't remove a task when scheduledTaskId is null`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - scheduledTaskId: null, - }, - }); - - await alertsClient.delete({ id: '1' }); - expect(taskManager.remove).not.toHaveBeenCalled(); - }); - - test(`doesn't invalidate API key when apiKey is null`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: null, - }, - }); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - test('swallows error when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'delete(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); - - await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"SOC Fail"` - ); - }); - - test('throws error when taskManager.remove throws an error', async () => { - taskManager.remove.mockRejectedValue(new Error('TM Fail')); - - await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"TM Fail"` - ); - }); - - describe('authorization', () => { - test('ensures user is authorised to delete this type of alert under the consumer', async () => { - await alertsClient.delete({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - - test('throws when user is not authorised to delete this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to delete a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to delete a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - }); -}); - -describe('update()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - enabled: true, - tags: ['foo'], - alertTypeId: 'myType', - schedule: { interval: '10s' }, - consumer: 'myApp', - scheduledTaskId: 'task-123', - params: {}, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - references: [], - version: '123', - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - alertTypeRegistry.get.mockReturnValue({ - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - }); - }); - - test('updates given parameters', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test2", - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": true, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - Object { - "actionRef": "action_1", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - Object { - "actionRef": "action_2", - "actionTypeId": "test2", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "myApp", - "enabled": true, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - Object { - "id": "1", - "name": "action_1", - "type": "action", - }, - Object { - "id": "2", - "name": "action_2", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it('calls the createApiKey function', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - apiKey: Buffer.from('123:abc').toString('base64'), - scheduledTaskId: 'task-123', - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "apiKey": "MTIzOmFiYw==", - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": true, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": "MTIzOmFiYw==", - "apiKeyOwner": "elastic", - "consumer": "myApp", - "enabled": true, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": "5m", - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it(`doesn't call the createAPIKey function when alert is disabled`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - enabled: false, - }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - apiKey: null, - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "apiKey": null, - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": false, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "myApp", - "enabled": false, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": "5m", - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it('should validate params', async () => { - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - validate: { - params: schema.object({ - param1: schema.string(), - }), - }, - async executor() {}, - producer: 'alerts', - }); - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"params invalid: [param1]: expected value of type [string] but got [undefined]"` - ); - }); - - it('should trim alert name in the API key name', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - name: ' my alert name ', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - apiKey: null, - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - ...existingAlert.attributes, - name: ' my alert name ', - }, - }); - - expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); - }); - - it('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - it('swallows error when getDecryptedAsInternalUser throws', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'update(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '234', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); - }); - - describe('updating an alert schedule', () => { - function mockApiCalls( - alertId: string, - taskId: string, - currentSchedule: IntervalSchedule, - updatedSchedule: IntervalSchedule - ) { - // mock return values from deps - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: alertId, - type: 'alert', - attributes: { - actions: [], - enabled: true, - alertTypeId: '123', - schedule: currentSchedule, - scheduledTaskId: 'task-123', - }, - references: [], - version: '123', - }); - - taskManager.schedule.mockResolvedValueOnce({ - id: taskId, - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: alertId, - type: 'alert', - attributes: { - enabled: true, - schedule: updatedSchedule, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: taskId, - }, - references: [ - { - name: 'action_0', - type: 'action', - id: alertId, - }, - ], - }); - - taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId })); - } - - test('updating the alert schedule should rerun the task immediately', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalledWith(taskId); - }); - - test('updating the alert without changing the schedule should not rerun the task', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).not.toHaveBeenCalled(); - }); - - test('updating the alert should not wait for the rerun the task to complete', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); - }); - - test('logs when the rerun of an alerts underlying task fails', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` - ); - }); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [], - }); - }); - - test('ensures user is authorised to update this type of alert under the consumer', async () => { - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [], - }, - }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); - }); - - test('throws when user is not authorised to update this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to update a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [], - }, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to update a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); - }); - }); -}); - -describe('updateApiKey()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - const existingEncryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '123', api_key: 'abc' }, - }); - }); - - test('updates the API key for the alert', async () => { - await alertsClient.updateApiKey({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - apiKey: Buffer.from('234:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - }, - { version: '123' } - ); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - apiKey: Buffer.from('234:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - }, - { version: '123' } - ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - }); - - test('swallows error when getting decrypted object throws', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' - ); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '234', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); - - await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail"` - ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); - }); - - describe('authorization', () => { - test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { - await alertsClient.updateApiKey({ id: '1' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - - test('throws when user is not authorised to updateApiKey this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - }); -}); - -describe('listAlertTypes', () => { - let alertsClient: AlertsClient; - const alertingAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'alertingAlertType', - name: 'alertingAlertType', - producer: 'alerts', - }; - const myAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myAppAlertType', - name: 'myAppAlertType', - producer: 'myApp', - }; - const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); - - const authorizedConsumers = { - alerts: { read: true, all: true }, - myApp: { read: true, all: true }, - myOtherApp: { read: true, all: true }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - test('should return a list of AlertTypes that exist in the registry', async () => { - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( - new Set([ - { ...myAppAlertType, authorizedConsumers }, - { ...alertingAlertType, authorizedConsumers }, - ]) - ); - expect(await alertsClient.listAlertTypes()).toEqual( - new Set([ - { ...myAppAlertType, authorizedConsumers }, - { ...alertingAlertType, authorizedConsumers }, - ]) - ); - }); - - describe('authorization', () => { - const listedTypes = new Set([ - { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }, - { - id: 'myOtherType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - }, - ]); - beforeEach(() => { - alertTypeRegistry.list.mockReturnValue(listedTypes); - }); - - test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { - const authorizedTypes = new Set([ - { - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: { - myApp: { read: true, all: true }, - }, - }, - ]); - authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); - - expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); - }); - }); -}); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts similarity index 96% rename from x-pack/plugins/alerts/server/alerts_client.ts rename to x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index bd278d39c6229..ef3a9e42b983f 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -14,8 +14,8 @@ import { SavedObject, PluginInitializerContext, } from 'src/core/server'; -import { esKuery } from '../../../../src/plugins/data/server'; -import { ActionsClient, ActionsAuthorization } from '../../actions/server'; +import { esKuery } from '../../../../../src/plugins/data/server'; +import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { Alert, PartialAlert, @@ -27,26 +27,26 @@ import { SanitizedAlert, AlertTaskState, AlertInstanceSummary, -} from './types'; -import { validateAlertTypeParams, alertExecutionStatusFromRaw } from './lib'; +} from '../types'; +import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, -} from '../../security/server'; -import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../task_manager/server'; -import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; -import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; -import { RegistryAlertType } from './alert_type_registry'; -import { AlertsAuthorization, WriteOperations, ReadOperations, and } from './authorization'; -import { IEventLogClient } from '../../../plugins/event_log/server'; -import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; -import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log'; -import { IEvent } from '../../event_log/server'; -import { parseDuration } from '../common/parse_duration'; -import { retryIfConflicts } from './lib/retry_if_conflicts'; -import { partiallyUpdateAlert } from './saved_objects'; +} from '../../../security/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../../task_manager/server'; +import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; +import { deleteTaskIfItExists } from '../lib/delete_task_if_it_exists'; +import { RegistryAlertType } from '../alert_type_registry'; +import { AlertsAuthorization, WriteOperations, ReadOperations, and } from '../authorization'; +import { IEventLogClient } from '../../../../plugins/event_log/server'; +import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; +import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; +import { IEvent } from '../../../event_log/server'; +import { parseDuration } from '../../common/parse_duration'; +import { retryIfConflicts } from '../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../saved_objects'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts b/x-pack/plugins/alerts/server/alerts_client/index.ts similarity index 83% rename from x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts rename to x-pack/plugins/alerts/server/alerts_client/index.ts index be3d074811032..e40076a29fffd 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts +++ b/x-pack/plugins/alerts/server/alerts_client/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -export const SOURCERER_FEATURE_FLAG_ON = true; +export * from './alerts_client'; diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts new file mode 100644 index 0000000000000..65a30d1750149 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -0,0 +1,1097 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { AlertsClient, ConstructorOptions, CreateOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsClientMock, actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +function getMockData(overwrites: Record = {}): CreateOptions['data'] { + return { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }; +} + +describe('create()', () => { + let alertsClient: AlertsClient; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + describe('authorization', () => { + function tryToExecuteOperation(options: CreateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + + return alertsClient.create(options); + } + + test('ensures user is authorised to create this type of alert under the consumer', async () => { + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('throws when user is not authorised to create this type of alert', async () => { + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to create a "myType" alert for "myApp"`) + ); + + await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to create a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + }); + + test('creates an alert', async () => { + const data = getMockData(); + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: createdAttributes, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + ...createdAttributes, + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + const result = await alertsClient.create({ data }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "consumer": "bar", + "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, + "id": "1", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "bar", + "createdAt": "2019-02-12T21:01:22.479Z", + "createdBy": "elastic", + "enabled": true, + "executionStatus": Object { + "error": null, + "lastExecutionDate": "2019-02-12T21:01:22.479Z", + "status": "pending", + }, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); + expect(taskManager.schedule).toHaveBeenCalledTimes(1); + expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "alertId": "1", + "spaceId": "default", + }, + "scope": Array [ + "alerting", + ], + "state": Object { + "alertInstances": Object {}, + "alertTypeState": Object {}, + "previousStartedAt": null, + }, + "taskType": "alerting:123", + }, + ] + `); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "scheduledTaskId": "task-123", + } + `); + }); + + test('creates an alert with multiple actions', async () => { + const data = getMockData({ + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [], + }); + const result = await alertsClient.create({ data }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + }); + + test('creates a disabled alert', async () => { + const data = getMockData({ enabled: false }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.create({ data }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": false, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": 10000, + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(taskManager.schedule).toHaveBeenCalledTimes(0); + }); + + test('should trim alert name when creating API key', async () => { + const data = getMockData({ name: ' my alert name ' }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.create({ data }); + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); + }); + + test('should validate params', async () => { + const data = getMockData(); + alertTypeRegistry.get.mockReturnValue({ + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + validate: { + params: schema.object({ + param1: schema.string(), + threshold: schema.number({ min: 0, max: 1 }), + }), + }, + async executor() {}, + producer: 'alerts', + }); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` + ); + }); + + test('throws error if loading actions fails', async () => { + const data = getMockData(); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test Error"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error and invalidates API key when create saved object fails', async () => { + const data = getMockData(); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test failure"` + ); + expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('attempts to remove saved object if scheduling failed', async () => { + const data = getMockData(); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test failure"` + ); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('returns task manager error if cleanup fails, logs to console', async () => { + const data = getMockData(); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); + unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( + new Error('Saved object delete error') + ); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Task manager error"` + ); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to cleanup alert "1" after scheduling task failed. Error: Saved object delete error' + ); + }); + + test('throws an error if alert type not registerd', async () => { + const data = getMockData(); + alertTypeRegistry.get.mockImplementation(() => { + throw new Error('Invalid type'); + }); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid type"` + ); + }); + + test('calls the API key function', async () => { + const data = getMockData(); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + await alertsClient.create({ data }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: { foo: true }, + }, + ], + alertTypeId: '123', + consumer: 'bar', + name: 'abc', + params: { bar: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', + createdBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + enabled: true, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + schedule: { interval: '10s' }, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + tags: ['foo'], + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + error: null, + }, + }, + { + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + } + ); + }); + + test(`doesn't create API key for disabled alerts`, async () => { + const data = getMockData({ enabled: false }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + await alertsClient.create({ data }); + + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: { foo: true }, + }, + ], + alertTypeId: '123', + consumer: 'bar', + name: 'abc', + params: { bar: true }, + apiKey: null, + apiKeyOwner: null, + createdBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + schedule: { interval: '10s' }, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + tags: ['foo'], + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + error: null, + }, + }, + { + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + } + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts new file mode 100644 index 0000000000000..1ebd9fc296b13 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('delete()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + actionTypeId: '.no-op', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.delete.mockResolvedValue({ + success: true, + }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + }); + + test('successfully removes an alert', async () => { + const result = await alertsClient.delete({ id: '1' }); + expect(result).toEqual({ success: true }); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + }); + + test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + const result = await alertsClient.delete({ id: '1' }); + expect(result).toEqual({ success: true }); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test(`doesn't remove a task when scheduledTaskId is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + scheduledTaskId: null, + }, + }); + + await alertsClient.delete({ id: '1' }); + expect(taskManager.remove).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate API key when apiKey is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: null, + }, + }); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"SOC Fail"` + ); + }); + + test('throws error when taskManager.remove throws an error', async () => { + taskManager.remove.mockRejectedValue(new Error('TM Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"TM Fail"` + ); + }); + + describe('authorization', () => { + test('ensures user is authorised to delete this type of alert under the consumer', async () => { + await alertsClient.delete({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('throws when user is not authorised to delete this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to delete a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts new file mode 100644 index 0000000000000..2dd3da07234ce --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('disable()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: true, + scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + version: '123', + references: [], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + }); + + describe('authorization', () => { + test('ensures user is authorised to disable this type of alert under the consumer', async () => { + await alertsClient.disable({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('throws when user is not authorised to disable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to disable a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + }); + + test('disables an alert', async () => { + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('falls back when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test(`doesn't disable already disabled alerts`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + actions: [], + enabled: false, + }, + }); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(taskManager.remove).not.toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate when no API key is used`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); + + await alertsClient.disable({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when failing to load decrypted saved object', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(taskManager.remove).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'disable(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); + + await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to update"` + ); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + test('throws when failing to remove task from task manager', async () => { + taskManager.remove.mockRejectedValueOnce(new Error('Failed to remove task')); + + await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to remove task"` + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts new file mode 100644 index 0000000000000..b214d8ba697b1 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -0,0 +1,361 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('enable()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + describe('authorization', () => { + beforeEach(() => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('ensures user is authorised to enable this type of alert under the consumer', async () => { + await alertsClient.enable({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to enable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to enable a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + }); + + test('enables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + updatedBy: 'elastic', + apiKey: null, + apiKeyOwner: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.schedule).toHaveBeenCalledWith({ + taskType: `alerting:myType`, + params: { + alertId: '1', + spaceId: 'default', + }, + state: { + alertInstances: {}, + alertTypeState: {}, + previousStartedAt: null, + }, + scope: ['alerting'], + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + scheduledTaskId: 'task-123', + }); + }); + + test('invalidates API key if ever one existed prior to updating', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test(`doesn't enable already enabled alerts`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('sets API key when createAPIKey returns one', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + }); + + test('falls back when failing to getDecryptedAsInternalUser', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'enable(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when failing to load the saved object using SOC', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to get"` + ); + expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error when failing to update the first time', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.update.mockReset(); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to update"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error when failing to update the second time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + }, + }); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + new Error('Fail to update second time') + ); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to update second time"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.schedule).toHaveBeenCalled(); + }); + + test('throws error when failing to schedule task', async () => { + taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to schedule"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts new file mode 100644 index 0000000000000..bf55a2070d8fe --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { nodeTypes } from '../../../../../../src/plugins/data/common'; +import { esKuery } from '../../../../../../src/plugins/data/server'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('find()', () => { + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]); + beforeEach(() => { + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, + }); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + score: 1, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + alertTypeRegistry.list.mockReturnValue(listedTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue( + new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]) + ); + }); + + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.find({ options: {} }); + expect(result).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "fields": undefined, + "filter": undefined, + "type": "alert", + }, + ] + `); + }); + + describe('authorization', () => { + test('ensures user is query filter types down to those the user is authorized to find', async () => { + const filter = esKuery.fromKueryExpression( + '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' + ); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter, + ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, + }); + + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.find({ options: { filter: 'someTerm' } }); + + const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; + expect(options.filter).toEqual( + nodeTypes.function.buildNode('and', [esKuery.fromKueryExpression('someTerm'), filter]) + ); + expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); + }); + + test('throws if user is not authorized to find any types', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized')); + await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot( + `"not authorized"` + ); + }); + + test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { + const ensureAlertTypeIsAuthorized = jest.fn(); + const logSuccessfulAuthorization = jest.fn(); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + }); + + unsecuredSavedObjectsClient.find.mockReset(); + unsecuredSavedObjectsClient.find.mockResolvedValue({ + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + actions: [], + alertTypeId: 'myType', + consumer: 'myApp', + tags: ['myTag'], + }, + score: 1, + references: [], + }, + ], + }); + + const alertsClient = new AlertsClient(alertsClientParams); + expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [], + "id": "1", + "schedule": undefined, + "tags": Array [ + "myTag", + ], + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ + fields: ['tags', 'alertTypeId', 'consumer'], + type: 'alert', + }); + expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); + expect(logSuccessfulAuthorization).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts new file mode 100644 index 0000000000000..327a1fa23ef05 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('get()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.get({ id: '1' }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test(`throws an error when references aren't found`, async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [], + }); + await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action reference \\"action_0\\" not found in alert id: 1"` + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.get({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts new file mode 100644 index 0000000000000..09212732b76e7 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -0,0 +1,292 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { eventLogClientMock } from '../../../../event_log/server/mocks'; +import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; +import { SavedObject } from 'kibana/server'; +import { EventsFactory } from '../../lib/alert_instance_summary_from_event_log.test'; +import { RawAlert } from '../../types'; +import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const eventLogClient = eventLogClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry, eventLogClient); +}); + +setGlobalDate(); + +const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { + page: 1, + per_page: 10000, + total: 0, + data: [], +}; + +const AlertInstanceSummaryIntervalSeconds = 1; + +const BaseAlertInstanceSummarySavedObject: SavedObject = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'alert-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: mockedDateString, + apiKey: null, + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + }, + references: [], +}; + +function getAlertInstanceSummarySavedObject( + attributes: Partial = {} +): SavedObject { + return { + ...BaseAlertInstanceSummarySavedObject, + attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, + }; +} + +describe('getAlertInstanceSummary()', () => { + let alertsClient: AlertsClient; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('runs as expected with some event log data', async () => { + const alertSO = getAlertInstanceSummarySavedObject({ + mutedInstanceIds: ['instance-muted-no-activity'], + }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); + + const eventsFactory = new EventsFactory(mockedDateString); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-currently-active') + .addNewInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .addActiveInstance('instance-previously-active') + .advanceTime(10000) + .addExecute() + .addResolvedInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .getEvents(); + const eventsResult = { + ...AlertInstanceSummaryFindEventsResult, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); + + const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); + + const result = await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + expect(result).toMatchInlineSnapshot(` + Object { + "alertTypeId": "123", + "consumer": "alert-consumer", + "enabled": true, + "errorMessages": Array [], + "id": "1", + "instances": Object { + "instance-currently-active": Object { + "activeStartDate": "2019-02-12T21:01:22.479Z", + "muted": false, + "status": "Active", + }, + "instance-muted-no-activity": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "instance-previously-active": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2019-02-12T21:01:32.479Z", + "muteAll": false, + "name": "alert-name", + "status": "Active", + "statusEndDate": "2019-02-12T21:01:22.479Z", + "statusStartDate": "2019-02-12T21:00:22.479Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": null, + } + `); + }); + + // Further tests don't check the result of `getAlertInstanceSummary()`, as the result + // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself + // has a complete set of tests. These tests just make sure the data gets + // sent into `getAlertInstanceSummary()` as appropriate. + + test('calls saved objects and event log client with default params', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + await alertsClient.getAlertInstanceSummary({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + ] + `); + // calculate the expected start/end date for one test + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + expect(end).toBe(mockedDateString); + + const startMillis = Date.parse(start!); + const endMillis = Date.parse(end!); + const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; + expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); + expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); + }); + + test('calls event log client with start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = new Date( + Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 + ).toISOString(); + await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T21:00:22.479Z", + } + `); + }); + + test('calls event log client with relative start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = '2m'; + await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T20:59:22.479Z", + } + `); + }); + + test('invalid start date throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = 'ain"t no way this will get parsed as a date'; + expect( + alertsClient.getAlertInstanceSummary({ id: '1', dateStart }) + ).rejects.toMatchInlineSnapshot( + `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` + ); + }); + + test('saved object get throws an error', async () => { + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + expect(alertsClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: OMG!]` + ); + }); + + test('findEvents throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); + + // error eaten but logged + await alertsClient.getAlertInstanceSummary({ id: '1' }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts new file mode 100644 index 0000000000000..42e573aea347f --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { TaskStatus } from '../../../../task_manager/server'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('getAlertState()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('gets the underlying task from TaskManager', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + + const scheduledTaskId = 'task-123'; + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + enabled: true, + scheduledTaskId, + mutedInstanceIds: [], + muteAll: true, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: scheduledTaskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: { + alertId: '1', + }, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(taskManager.get).toHaveBeenCalledTimes(1); + expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.getAlertState({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); + }); + + test('throws when user is not authorised to getAlertState this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + // `get` check + authorization.ensureAuthorized.mockResolvedValueOnce(); + // `getAlertState` check + authorization.ensureAuthorized.mockRejectedValueOnce( + new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts new file mode 100644 index 0000000000000..96e49e21b9045 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TaskManager } from '../../../../task_manager/server/task_manager'; +import { IEventLogClient } from '../../../../event_log/server'; +import { actionsClientMock } from '../../../../actions/server/mocks'; +import { ConstructorOptions } from '../alerts_client'; +import { eventLogClientMock } from '../../../../event_log/server/mocks'; +import { AlertTypeRegistry } from '../../alert_type_registry'; + +export const mockedDateString = '2019-02-12T21:01:22.479Z'; + +export function setGlobalDate() { + const mockedDate = new Date(mockedDateString); + const DateOriginal = Date; + // A version of date that responds to `new Date(null|undefined)` and `Date.now()` + // by returning a fixed date, otherwise should be same as Date. + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (global as any).Date = class Date { + constructor(...args: unknown[]) { + // sometimes the ctor has no args, sometimes has a single `null` arg + if (args[0] == null) { + // @ts-ignore + return mockedDate; + } else { + // @ts-ignore + return new DateOriginal(...args); + } + } + static now() { + return mockedDate.getTime(); + } + static parse(string: string) { + return DateOriginal.parse(string); + } + }; +} + +export function getBeforeSetup( + alertsClientParams: jest.Mocked, + taskManager: jest.Mocked< + Pick + >, + alertTypeRegistry: jest.Mocked>, + eventLogClient?: jest.Mocked +) { + jest.resetAllMocks(); + alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); + alertsClientParams.invalidateAPIKey.mockResolvedValue({ + apiKeysEnabled: true, + result: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + }, + }); + alertsClientParams.getUserName.mockResolvedValue('elastic'); + taskManager.runNow.mockResolvedValue({ id: '' }); + const actionsClient = actionsClientMock.create(); + + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + isPreconfigured: false, + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + { + id: '2', + isPreconfigured: false, + actionTypeId: 'test2', + name: 'test2', + config: { + foo: 'bar', + }, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + + alertTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + })); + alertsClientParams.getEventLogClient.mockResolvedValue( + eventLogClient ?? eventLogClientMock.create() + ); +} diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts new file mode 100644 index 0000000000000..4337ed6c491d4 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('listAlertTypes', () => { + let alertsClient: AlertsClient; + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerts', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + const authorizedConsumers = { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('should return a list of AlertTypes that exist in the registry', async () => { + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue( + new Set([ + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, + ]) + ); + expect(await alertsClient.listAlertTypes()).toEqual( + new Set([ + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, + ]) + ); + }); + + describe('authorization', () => { + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + { + id: 'myOtherType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + }, + ]); + beforeEach(() => { + alertTypeRegistry.list.mockReturnValue(listedTypes); + }); + + test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { + const authorizedTypes = new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]); + authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); + + expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts new file mode 100644 index 0000000000000..44ee6713f2560 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('muteAll()', () => { + test('mutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + muteAll: false, + }, + references: [], + version: '123', + }); + + await alertsClient.muteAll({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: true, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to muteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to muteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts new file mode 100644 index 0000000000000..dc9a1600a5776 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('muteInstance()', () => { + test('mutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: ['2'], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + test('skips muting when alert instance already muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + test('skips muting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('throws when user is not authorised to muteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts new file mode 100644 index 0000000000000..45920db105c2a --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('unmuteAll()', () => { + test('unmutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + muteAll: true, + }, + references: [], + version: '123', + }); + + await alertsClient.unmuteAll({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: false, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to unmuteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts new file mode 100644 index 0000000000000..5604011501130 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('unmuteInstance()', () => { + test('unmutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + version: '123', + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { version: '123' } + ); + }); + + test('skips unmuting when alert instance not muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + test('skips unmuting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('throws when user is not authorised to unmuteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts new file mode 100644 index 0000000000000..14275575f75f4 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -0,0 +1,1257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { schema } from '@kbn/config-schema'; +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { IntervalSchedule } from '../../types'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { resolvable } from '../../test_utils'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('update()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '10s' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: {}, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + references: [], + version: '123', + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + alertTypeRegistry.get.mockReturnValue({ + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + }); + + test('updates given parameters', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": true, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_1", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_2", + "actionTypeId": "test2", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "myApp", + "enabled": true, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + Object { + "id": "1", + "name": "action_1", + "type": "action", + }, + Object { + "id": "2", + "name": "action_2", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it('calls the createApiKey function', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + apiKey: Buffer.from('123:abc').toString('base64'), + scheduledTaskId: 'task-123', + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": true, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": "MTIzOmFiYw==", + "apiKeyOwner": "elastic", + "consumer": "myApp", + "enabled": true, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": "5m", + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it(`doesn't call the createAPIKey function when alert is disabled`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + enabled: false, + }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": null, + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": false, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "myApp", + "enabled": false, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": "5m", + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it('should validate params', async () => { + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + validate: { + params: schema.object({ + param1: schema.string(), + }), + }, + async executor() {}, + producer: 'alerts', + }); + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` + ); + }); + + it('should trim alert name in the API key name', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + ...existingAlert.attributes, + name: ' my alert name ', + }, + }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); + }); + + it('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + it('swallows error when getDecryptedAsInternalUser throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'update(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '234', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + }); + + describe('updating an alert schedule', () => { + function mockApiCalls( + alertId: string, + taskId: string, + currentSchedule: IntervalSchedule, + updatedSchedule: IntervalSchedule + ) { + // mock return values from deps + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + actions: [], + enabled: true, + alertTypeId: '123', + schedule: currentSchedule, + scheduledTaskId: 'task-123', + }, + references: [], + version: '123', + }); + + taskManager.schedule.mockResolvedValueOnce({ + id: taskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + enabled: true, + schedule: updatedSchedule, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: taskId, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: alertId, + }, + ], + }); + + taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId })); + } + + test('updating the alert schedule should rerun the task immediately', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalledWith(taskId); + }); + + test('updating the alert without changing the schedule should not rerun the task', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).not.toHaveBeenCalled(); + }); + + test('updating the alert should not wait for the rerun the task to complete', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); + }); + + test('logs when the rerun of an alerts underlying task fails', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` + ); + }); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [], + }); + }); + + test('ensures user is authorised to update this type of alert under the consumer', async () => { + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('throws when user is not authorised to update this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to update a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts new file mode 100644 index 0000000000000..97ddfa5e4adb4 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/task_manager.mock'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.start(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('updateApiKey()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + const existingEncryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '123', api_key: 'abc' }, + }); + }); + + test('updates the API key for the alert', async () => { + await alertsClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + }); + + test('swallows error when getting decrypted object throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '234', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + + await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail"` + ); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + }); + + describe('authorization', () => { + test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { + await alertsClient.updateApiKey({ id: '1' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('throws when user is not authorised to updateApiKey this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 9f7a911bf21c7..ccd5f143440da 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -505,600 +505,6 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the } } } - }, - "otlp": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/cpp": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/erlang": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/go": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/java": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/php": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/python": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } - }, - "opentelemetry/webjs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "language": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "composite": { - "type": "keyword" - } - } - } - } - } - } } } }, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx index 0816541865362..d20aae29fb8ce 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// import { storiesOf } from '@storybook/react'; + import { cloneDeep, merge } from 'lodash'; -import React from 'react'; +import React, { ComponentType } from 'react'; +import { MemoryRouter, Route } from 'react-router-dom'; import { TransactionDurationAlertTrigger } from '.'; import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; import { @@ -14,40 +15,55 @@ import { } from '../../../context/ApmPluginContext/MockApmPluginContext'; import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; -// Disabling this because we currently don't have a way to mock `useEnvironments` -// which is used by this component. Using the fetch-mock module should work, but -// our current storybook setup has core-js-related problems when trying to import -// it. -// storiesOf('app/TransactionDurationAlertTrigger', module).add('example', -// eslint-disable-next-line @typescript-eslint/no-unused-expressions -() => { +export default { + title: 'app/TransactionDurationAlertTrigger', + component: TransactionDurationAlertTrigger, + decorators: [ + (Story: ComponentType) => { + const contextMock = (merge(cloneDeep(mockApmPluginContextValue), { + core: { + http: { + get: (endpoint: string) => { + if (endpoint === '/api/apm/ui_filters/environments') { + return Promise.resolve(['production']); + } else { + return Promise.resolve({ + transactionTypes: ['request'], + }); + } + }, + }, + }, + }) as unknown) as ApmPluginContextValue; + + return ( +
+ + + + + + + + + +
+ ); + }, + ], +}; + +export function Example() { const params = { threshold: 1500, aggregationType: 'avg' as const, window: '5m', }; - - const contextMock = (merge(cloneDeep(mockApmPluginContextValue), { - core: { - http: { - get: () => { - return Promise.resolve({ transactionTypes: ['request'] }); - }, - }, - }, - }) as unknown) as ApmPluginContextValue; - return ( -
- - - undefined} - setAlertProperty={() => undefined} - /> - - -
+ undefined} + setAlertProperty={() => undefined} + /> ); -}; +} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx index 5ad6fd547169d..ff95d6fd1254c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx @@ -4,804 +4,2538 @@ * you may not use this file except in compliance with the Elastic License. */ -import { storiesOf } from '@storybook/react'; -import React from 'react'; +import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; import { ExceptionStacktrace } from './ExceptionStacktrace'; -storiesOf('app/ErrorGroupDetails/DetailView/ExceptionStacktrace', module) - .addDecorator((storyFn) => { - return {storyFn()}; - }) - .add('JavaScript with some context', () => { - const exceptions: Exception[] = [ - { - code: '503', - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/elastic-apm-http-client/index.js', - abs_path: '/app/node_modules/elastic-apm-http-client/index.js', - line: { - number: 711, - context: - " const err = new Error('Unexpected APM Server response when polling config')", - }, - function: 'processConfigErrorResponse', - context: { - pre: ['', 'function processConfigErrorResponse (res, buf) {'], - post: ['', ' err.code = res.statusCode'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/elastic-apm-http-client/index.js', - abs_path: '/app/node_modules/elastic-apm-http-client/index.js', - line: { - number: 196, - context: - ' res.destroy(processConfigErrorResponse(res, buf))', - }, - function: '', - context: { - pre: [' }', ' } else {'], - post: [' }', ' })'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/fast-stream-to-buffer/index.js', - abs_path: '/app/node_modules/fast-stream-to-buffer/index.js', - line: { - number: 20, - context: ' cb(err, buffers[0], stream)', - }, - function: 'IncomingMessage.', - context: { - pre: [' break', ' case 1:'], - post: [' break', ' default:'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/once/once.js', - abs_path: '/app/node_modules/once/once.js', - line: { - number: 25, - context: ' return f.value = fn.apply(this, arguments)', - }, - function: 'f', - context: { - pre: [' if (f.called) return f.value', ' f.called = true'], - post: [' }', ' f.called = false'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/end-of-stream/index.js', - abs_path: '/app/node_modules/end-of-stream/index.js', - line: { - number: 36, - context: '\t\tif (!writable) callback.call(stream);', - }, - function: 'onend', - context: { - pre: ['\tvar onend = function() {', '\t\treadable = false;'], - post: ['\t};', ''], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: 'events.js', - filename: 'events.js', - line: { - number: 327, - }, - function: 'emit', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: '_stream_readable.js', - abs_path: '_stream_readable.js', - line: { - number: 1220, - }, - function: 'endReadableNT', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'internal/process/task_queues.js', - abs_path: 'internal/process/task_queues.js', - line: { - number: 84, - }, - function: 'processTicksAndRejections', - }, - ], - module: 'elastic-apm-http-client', - handled: false, - attributes: { - response: - '\r\n503 Service Temporarily Unavailable\r\n\r\n

503 Service Temporarily Unavailable

\r\n
nginx/1.17.7
\r\n\r\n\r\n', - }, - type: 'Error', - message: 'Unexpected APM Server response when polling config', - }, - ]; +export default { + title: 'app/ErrorGroupDetails/DetailView/ExceptionStacktrace', + component: ExceptionStacktrace, + decorators: [ + (Story: ComponentType) => { + return ( + + + + ); + }, + ], +}; +export function JavaWithLongLines() { + const exceptions: Exception[] = [ + { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 296, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeAndHandle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + function: 'handle', + module: 'org.springframework.web.servlet.mvc.method', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + function: 'service', + module: 'javax.servlet.http', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardContextValve.java', + classname: 'org.apache.catalina.core.StandardContextValve', + line: { + number: 96, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AuthenticatorBase.java', + classname: 'org.apache.catalina.authenticator.AuthenticatorBase', + line: { + number: 496, + }, + module: 'org.apache.catalina.authenticator', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardHostValve.java', + classname: 'org.apache.catalina.core.StandardHostValve', + line: { + number: 140, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ErrorReportValve.java', + classname: 'org.apache.catalina.valves.ErrorReportValve', + line: { + number: 81, + }, + module: 'org.apache.catalina.valves', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardEngineValve.java', + classname: 'org.apache.catalina.core.StandardEngineValve', + line: { + number: 87, + }, + function: 'invoke', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CoyoteAdapter.java', + classname: 'org.apache.catalina.connector.CoyoteAdapter', + line: { + number: 342, + }, + module: 'org.apache.catalina.connector', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'Http11Processor.java', + classname: 'org.apache.coyote.http11.Http11Processor', + line: { + number: 803, + }, + module: 'org.apache.coyote.http11', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractProcessorLight.java', + classname: 'org.apache.coyote.AbstractProcessorLight', + line: { + number: 66, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractProtocol.java', + classname: 'org.apache.coyote.AbstractProtocol$ConnectionHandler', + line: { + number: 790, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'NioEndpoint.java', + classname: 'org.apache.tomcat.util.net.NioEndpoint$SocketProcessor', + line: { + number: 1468, + }, + function: 'doRun', + module: 'org.apache.tomcat.util.net', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'SocketProcessorBase.java', + classname: 'org.apache.tomcat.util.net.SocketProcessorBase', + line: { + number: 49, + }, + module: 'org.apache.tomcat.util.net', + function: 'run', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'TaskThread.java', + classname: + 'org.apache.tomcat.util.threads.TaskThread$WrappingRunnable', + line: { + number: 61, + }, + function: 'run', + module: 'org.apache.tomcat.util.threads', + }, + ], + type: + 'org.springframework.http.converter.HttpMessageNotWritableException', + message: + 'Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats["numbers"]->com.sun.proxy.$Proxy128["revenue"])', + }, + { + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'JsonMappingException.java', + classname: 'com.fasterxml.jackson.databind.JsonMappingException', + line: { + number: 391, + }, + module: 'com.fasterxml.jackson.databind', + function: 'wrapWithPath', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'JsonMappingException.java', + classname: 'com.fasterxml.jackson.databind.JsonMappingException', + line: { + number: 351, + }, + module: 'com.fasterxml.jackson.databind', + function: 'wrapWithPath', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StdSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.std.StdSerializer', + line: { + number: 316, + }, + function: 'wrapAndThrow', + module: 'com.fasterxml.jackson.databind.ser.std', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 480, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: '_serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 319, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter$Prefetch', + line: { + number: 1396, + }, + module: 'com.fasterxml.jackson.databind', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter', + line: { + number: 913, + }, + module: 'com.fasterxml.jackson.databind', + function: 'writeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 286, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeAndHandle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + function: 'handleInternal', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + module: 'org.springframework.web.servlet.mvc.method', + function: 'handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + function: 'doFilter', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + ], + message: + 'Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats["numbers"]->com.sun.proxy.$Proxy128["revenue"])', + type: 'com.fasterxml.jackson.databind.JsonMappingException', + }, + { + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'JdkDynamicAopProxy.java', + classname: 'org.springframework.aop.framework.JdkDynamicAopProxy', + line: { + number: 226, + }, + module: 'org.springframework.aop.framework', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 688, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 480, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: '_serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 319, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter$Prefetch', + line: { + number: 1396, + }, + module: 'com.fasterxml.jackson.databind', + function: 'serialize', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter', + line: { + number: 913, + }, + module: 'com.fasterxml.jackson.databind', + function: 'writeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 286, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + function: 'handleReturnValue', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + function: 'invokeAndHandle', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + module: 'org.springframework.web.servlet.mvc.method', + function: 'handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + function: 'doFilter', + module: 'org.springframework.web.filter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + function: 'doFilter', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardContextValve.java', + classname: 'org.apache.catalina.core.StandardContextValve', + line: { + number: 96, + }, + function: 'invoke', + module: 'org.apache.catalina.core', + }, + ], + message: + 'Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue()', + type: 'org.springframework.aop.AopInvocationException', + }, + ]; + + return ; +} +JavaWithLongLines.decorators = [ + (Story: ComponentType) => { return ( - +
+ +
); - }) - .add('Ruby with context and library frames', () => { - const exceptions: Exception[] = [ - { - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_record/core.rb', - abs_path: - '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/core.rb', - line: { - number: 177, - }, - function: 'find', - }, - { - library_frame: false, - exclude_from_grouping: false, - filename: 'api/orders_controller.rb', - abs_path: '/app/app/controllers/api/orders_controller.rb', - line: { - number: 23, - context: ' render json: Order.find(params[:id])\n', - }, - function: 'show', - context: { - pre: ['\n', ' def show\n'], - post: [' end\n', ' end\n'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/basic_implicit_render.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/basic_implicit_render.rb', - line: { - number: 6, - }, - function: 'send_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/base.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', - line: { - number: 194, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/rendering.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rendering.rb', - line: { - number: 30, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', - line: { - number: 42, - }, - function: 'block in process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', - line: { - number: 132, - }, - function: 'run_callbacks', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', - line: { - number: 41, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rescue.rb', - filename: 'action_controller/metal/rescue.rb', - line: { - number: 22, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', - filename: 'action_controller/metal/instrumentation.rb', - line: { - number: 34, - }, - function: 'block in process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', - line: { - number: 168, - }, - function: 'block in instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications/instrumenter.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications/instrumenter.rb', - line: { - number: 23, - }, - function: 'instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', - line: { - number: 168, - }, - function: 'instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/instrumentation.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', - line: { - number: 32, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/params_wrapper.rb', - filename: 'action_controller/metal/params_wrapper.rb', - line: { - number: 256, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_record/railties/controller_runtime.rb', - abs_path: - '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/railties/controller_runtime.rb', - line: { - number: 24, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/base.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', - line: { - number: 134, - }, - function: 'process', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_view/rendering.rb', - abs_path: - '/usr/local/bundle/gems/actionview-5.2.4.1/lib/action_view/rendering.rb', - line: { - number: 32, - }, - function: 'process', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', - line: { - number: 191, - }, - function: 'dispatch', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', - filename: 'action_controller/metal.rb', - line: { - number: 252, - }, - function: 'dispatch', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 52, - }, - function: 'dispatch', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 34, - }, - function: 'serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - filename: 'action_dispatch/journey/router.rb', - line: { - number: 52, - }, - function: 'block in serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/journey/router.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - line: { - number: 35, - }, - function: 'each', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/journey/router.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - line: { - number: 35, - }, - function: 'serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 840, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rack/static.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/static.rb', - line: { - number: 161, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/tempfile_reaper.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb', - line: { - number: 15, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/etag.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/conditional_get.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/head.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb', - line: { - number: 12, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/http/content_security_policy.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/http/content_security_policy.rb', - line: { - number: 18, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rack/session/abstract/id.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', - line: { - number: 266, - }, - function: 'context', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/session/abstract/id.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', - line: { - number: 260, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/cookies.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/cookies.rb', - line: { - number: 670, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', - line: { - number: 28, - }, - function: 'block in call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', - line: { - number: 98, - }, - function: 'run_callbacks', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', - line: { - number: 26, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/debug_exceptions.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/debug_exceptions.rb', - line: { - number: 61, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'action_dispatch/middleware/show_exceptions.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/show_exceptions.rb', - line: { - number: 33, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'lograge/rails_ext/rack/logger.rb', - abs_path: - '/usr/local/bundle/gems/lograge-0.11.2/lib/lograge/rails_ext/rack/logger.rb', - line: { - number: 15, - }, - function: 'call_app', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rails/rack/logger.rb', - abs_path: - '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/rack/logger.rb', - line: { - number: 28, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/remote_ip.rb', - filename: 'action_dispatch/middleware/remote_ip.rb', - line: { - number: 81, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'request_store/middleware.rb', - abs_path: - '/usr/local/bundle/gems/request_store-1.5.0/lib/request_store/middleware.rb', - line: { - number: 19, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/request_id.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/request_id.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/method_override.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb', - line: { - number: 24, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/runtime.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb', - line: { - number: 22, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/cache/strategy/local_cache_middleware.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/cache/strategy/local_cache_middleware.rb', - line: { - number: 29, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/executor.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/executor.rb', - line: { - number: 14, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/static.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/static.rb', - line: { - number: 127, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/sendfile.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb', - line: { - number: 110, - }, - function: 'call', - }, - { - library_frame: false, - exclude_from_grouping: false, - filename: 'opbeans_shuffle.rb', - abs_path: '/app/lib/opbeans_shuffle.rb', - line: { - number: 32, - context: ' @app.call(env)\n', - }, - function: 'call', - context: { - pre: [' end\n', ' else\n'], - post: [' end\n', ' rescue Timeout::Error\n'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'elastic_apm/middleware.rb', - abs_path: - '/usr/local/bundle/gems/elastic-apm-3.8.0/lib/elastic_apm/middleware.rb', - line: { - number: 36, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rails/engine.rb', - abs_path: - '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/engine.rb', - line: { - number: 524, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/configuration.rb', - abs_path: - '/usr/local/bundle/gems/puma-4.3.5/lib/puma/configuration.rb', - line: { - number: 228, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 713, - }, - function: 'handle_request', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 472, - }, - function: 'process_client', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 328, - }, - function: 'block in run', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/thread_pool.rb', - abs_path: - '/usr/local/bundle/gems/puma-4.3.5/lib/puma/thread_pool.rb', - line: { - number: 134, - }, - function: 'block in spawn_thread', - }, - ], - handled: false, - module: 'ActiveRecord', - message: "Couldn't find Order with 'id'=956", - type: 'ActiveRecord::RecordNotFound', + }, +]; + +export function JavaScriptWithSomeContext() { + const exceptions: Exception[] = [ + { + code: '503', + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 711, + context: + " const err = new Error('Unexpected APM Server response when polling config')", + }, + function: 'processConfigErrorResponse', + context: { + pre: ['', 'function processConfigErrorResponse (res, buf) {'], + post: ['', ' err.code = res.statusCode'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 196, + context: + ' res.destroy(processConfigErrorResponse(res, buf))', + }, + function: '', + context: { + pre: [' }', ' } else {'], + post: [' }', ' })'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/fast-stream-to-buffer/index.js', + abs_path: '/app/node_modules/fast-stream-to-buffer/index.js', + line: { + number: 20, + context: ' cb(err, buffers[0], stream)', + }, + function: 'IncomingMessage.', + context: { + pre: [' break', ' case 1:'], + post: [' break', ' default:'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/once/once.js', + abs_path: '/app/node_modules/once/once.js', + line: { + number: 25, + context: ' return f.value = fn.apply(this, arguments)', + }, + function: 'f', + context: { + pre: [' if (f.called) return f.value', ' f.called = true'], + post: [' }', ' f.called = false'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/end-of-stream/index.js', + abs_path: '/app/node_modules/end-of-stream/index.js', + line: { + number: 36, + context: '\t\tif (!writable) callback.call(stream);', + }, + function: 'onend', + context: { + pre: ['\tvar onend = function() {', '\t\treadable = false;'], + post: ['\t};', ''], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: 'events.js', + filename: 'events.js', + line: { + number: 327, + }, + function: 'emit', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: '_stream_readable.js', + abs_path: '_stream_readable.js', + line: { + number: 1220, + }, + function: 'endReadableNT', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'internal/process/task_queues.js', + abs_path: 'internal/process/task_queues.js', + line: { + number: 84, + }, + function: 'processTicksAndRejections', + }, + ], + module: 'elastic-apm-http-client', + handled: false, + attributes: { + response: + '\r\n503 Service Temporarily Unavailable\r\n\r\n

503 Service Temporarily Unavailable

\r\n
nginx/1.17.7
\r\n\r\n\r\n', }, - ]; + type: 'Error', + message: 'Unexpected APM Server response when polling config', + }, + ]; + + return ( + + ); +} +JavaScriptWithSomeContext.storyName = 'JavaScript With Some Context'; + +export function RubyWithContextAndLibraryFrames() { + const exceptions: Exception[] = [ + { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/core.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/core.rb', + line: { + number: 177, + }, + function: 'find', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'api/orders_controller.rb', + abs_path: '/app/app/controllers/api/orders_controller.rb', + line: { + number: 23, + context: ' render json: Order.find(params[:id])\n', + }, + function: 'show', + context: { + pre: ['\n', ' def show\n'], + post: [' end\n', ' end\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/basic_implicit_render.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/basic_implicit_render.rb', + line: { + number: 6, + }, + function: 'send_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 194, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rendering.rb', + line: { + number: 30, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 42, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 132, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 41, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rescue.rb', + filename: 'action_controller/metal/rescue.rb', + line: { + number: 22, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + filename: 'action_controller/metal/instrumentation.rb', + line: { + number: 34, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'block in instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications/instrumenter.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications/instrumenter.rb', + line: { + number: 23, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/instrumentation.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + line: { + number: 32, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/params_wrapper.rb', + filename: 'action_controller/metal/params_wrapper.rb', + line: { + number: 256, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/railties/controller_runtime.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/railties/controller_runtime.rb', + line: { + number: 24, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 134, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_view/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionview-5.2.4.1/lib/action_view/rendering.rb', + line: { + number: 32, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + line: { + number: 191, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + filename: 'action_controller/metal.rb', + line: { + number: 252, + }, + function: 'dispatch', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 52, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 34, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + filename: 'action_dispatch/journey/router.rb', + line: { + number: 52, + }, + function: 'block in serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'each', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 840, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/static.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/static.rb', + line: { + number: 161, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/tempfile_reaper.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb', + line: { + number: 15, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/etag.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/conditional_get.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/head.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb', + line: { + number: 12, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/http/content_security_policy.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/http/content_security_policy.rb', + line: { + number: 18, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 266, + }, + function: 'context', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 260, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/cookies.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/cookies.rb', + line: { + number: 670, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 28, + }, + function: 'block in call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 98, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 26, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/debug_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/debug_exceptions.rb', + line: { + number: 61, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/middleware/show_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/show_exceptions.rb', + line: { + number: 33, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'lograge/rails_ext/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/lograge-0.11.2/lib/lograge/rails_ext/rack/logger.rb', + line: { + number: 15, + }, + function: 'call_app', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rails/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/rack/logger.rb', + line: { + number: 28, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/remote_ip.rb', + filename: 'action_dispatch/middleware/remote_ip.rb', + line: { + number: 81, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'request_store/middleware.rb', + abs_path: + '/usr/local/bundle/gems/request_store-1.5.0/lib/request_store/middleware.rb', + line: { + number: 19, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/request_id.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/request_id.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/method_override.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb', + line: { + number: 24, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/runtime.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb', + line: { + number: 22, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/cache/strategy/local_cache_middleware.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/cache/strategy/local_cache_middleware.rb', + line: { + number: 29, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/executor.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/executor.rb', + line: { + number: 14, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/static.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/static.rb', + line: { + number: 127, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/sendfile.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb', + line: { + number: 110, + }, + function: 'call', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'opbeans_shuffle.rb', + abs_path: '/app/lib/opbeans_shuffle.rb', + line: { + number: 32, + context: ' @app.call(env)\n', + }, + function: 'call', + context: { + pre: [' end\n', ' else\n'], + post: [' end\n', ' rescue Timeout::Error\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm/middleware.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-3.8.0/lib/elastic_apm/middleware.rb', + line: { + number: 36, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rails/engine.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/engine.rb', + line: { + number: 524, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/configuration.rb', + abs_path: + '/usr/local/bundle/gems/puma-4.3.5/lib/puma/configuration.rb', + line: { + number: 228, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 713, + }, + function: 'handle_request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 472, + }, + function: 'process_client', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 328, + }, + function: 'block in run', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/thread_pool.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/thread_pool.rb', + line: { + number: 134, + }, + function: 'block in spawn_thread', + }, + ], + handled: false, + module: 'ActiveRecord', + message: "Couldn't find Order with 'id'=956", + type: 'ActiveRecord::RecordNotFound', + }, + ]; - return ; - }); + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index d65ce1879ce02..7b944ed1b6ceb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -84,6 +84,11 @@ function CytoscapeComponent({ cy.elements().forEach((element) => { if (!elementIds.includes(element.data('id'))) { cy.remove(element); + } else { + // Doing an "add" with an element with the same id will keep the original + // element. Set the data with the new element data. + const newElement = elements.find((el) => el.data.id === element.id()); + element.data(newElement?.data ?? element.data()); } }); cy.trigger('custom:data', [fit]); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 7771a232a5c9e..d0902c427aac8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -10,7 +10,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; -import React from 'react'; +import React, { Fragment } from 'react'; import styled from 'styled-components'; import { SPAN_SUBTYPE, @@ -71,7 +71,7 @@ export function Info(data: InfoProps) { resource.label || resource['span.destination.service.resource']; const desc = `${resource['span.type']} (${resource['span.subtype']})`; return ( - <> + {desc} - + ); })} @@ -97,8 +97,8 @@ export function Info(data: InfoProps) { {listItems.map( ({ title, description }) => description && ( -
- +
+ {title} diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index dfeb537b04865..6632b22b59969 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -27,10 +27,12 @@ const FileDetails = styled.div` const LibraryFrameFileDetail = styled.span` color: ${({ theme }) => theme.eui.euiColorDarkShade}; + word-break: break-word; `; const AppFrameFileDetail = styled.span` color: ${({ theme }) => theme.eui.euiColorFullShade}; + word-break: break-word; `; interface Props { diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index b0083da69cf85..cf17c9dbbf2e3 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -122,11 +122,69 @@ async function init() { }); await createRole({ roleName: KIBANA_READ_ROLE, - kibanaPrivileges: { base: ['read'] }, + kibanaPrivileges: { + feature: { + // core + discover: ['read'], + dashboard: ['read'], + canvas: ['read'], + ml: ['read'], + maps: ['read'], + graph: ['read'], + visualize: ['read'], + + // observability + logs: ['read'], + infrastructure: ['read'], + apm: ['read'], + uptime: ['read'], + + // security + siem: ['read'], + + // management + dev_tools: ['read'], + advancedSettings: ['read'], + indexPatterns: ['read'], + savedObjectsManagement: ['read'], + stackAlerts: ['read'], + ingestManager: ['read'], + actions: ['read'], + }, + }, }); await createRole({ roleName: KIBANA_WRITE_ROLE, - kibanaPrivileges: { base: ['all'] }, + kibanaPrivileges: { + feature: { + // core + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + ml: ['all'], + maps: ['all'], + graph: ['all'], + visualize: ['all'], + + // observability + logs: ['all'], + infrastructure: ['all'], + apm: ['all'], + uptime: ['all'], + + // security + siem: ['all'], + + // management + dev_tools: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + savedObjectsManagement: ['all'], + stackAlerts: ['all'], + ingestManager: ['all'], + actions: ['all'], + }, + }, }); // read access only to APM + apm index access diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts index 4bbda9add0fdb..ba2f7617c5108 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -12,7 +12,7 @@ import { TimeframeMap1d, TimeframeMapAll, } from './types'; -import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; +import { ElasticAgentName } from '../../../typings/es_schemas/ui/fields/agent'; const long: { type: 'long' } = { type: 'long' }; @@ -34,7 +34,7 @@ const timeframeMapSchema: MakeSchemaFrom = { ...timeframeMapAllSchema, }; -const agentSchema: MakeSchemaFrom['agents'][AgentName] = { +const agentSchema: MakeSchemaFrom['agents'][ElasticAgentName] = { agent: { version: { type: 'array', items: { type: 'keyword' } }, }, @@ -101,17 +101,6 @@ const apmPerAgentSchema: Pick< python: agentSchema, ruby: agentSchema, 'rum-js': agentSchema, - otlp: agentSchema, - 'opentelemetry/cpp': agentSchema, - 'opentelemetry/dotnet': agentSchema, - 'opentelemetry/erlang': agentSchema, - 'opentelemetry/go': agentSchema, - 'opentelemetry/java': agentSchema, - 'opentelemetry/nodejs': agentSchema, - 'opentelemetry/php': agentSchema, - 'opentelemetry/python': agentSchema, - 'opentelemetry/ruby': agentSchema, - 'opentelemetry/webjs': agentSchema, }, }; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts index 7ed79752b43c4..5d0f3172a4806 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts @@ -5,7 +5,10 @@ */ import { DeepPartial } from 'utility-types'; -import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; +import { + AgentName, + ElasticAgentName, +} from '../../../typings/es_schemas/ui/fields/agent'; export interface TimeframeMap { '1d': number; @@ -86,7 +89,7 @@ export interface APMUsage { }; }; agents: Record< - AgentName, + ElasticAgentName, { agent: { version: string[]; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 092485c46fb08..4cb0c4c750dd1 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '@kbn/logging'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { PromiseReturnType } from '../../../../typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -23,9 +24,11 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems({ setup, searchAggregatedTransactions, + logger, }: { setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; + logger: Logger; }) { const params = { projection: getServicesProjection({ @@ -49,7 +52,10 @@ export async function getServicesItems({ getTransactionRates(params), getTransactionErrorRates(params), getEnvironments(params), - getHealthStatuses(params, setup.uiFilters.environment), + getHealthStatuses(params, setup.uiFilters.environment).catch((err) => { + logger.error(err); + return []; + }), ]); const allMetrics = [ diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 04744a9c791bb..5f39d6c836930 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -5,6 +5,7 @@ */ import { isEmpty } from 'lodash'; +import { Logger } from '@kbn/logging'; import { PromiseReturnType } from '../../../../typings/common'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { hasHistoricalAgentData } from './has_historical_agent_data'; @@ -16,14 +17,17 @@ export type ServiceListAPIResponse = PromiseReturnType; export async function getServices({ setup, searchAggregatedTransactions, + logger, }: { setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; + logger: Logger; }) { const [items, hasLegacyData] = await Promise.all([ getServicesItems({ setup, searchAggregatedTransactions, + logger, }), getLegacyDataStatus(setup), ]); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 11adbe894f779..8491f536342b8 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -47,7 +47,11 @@ describe('services queries', () => { it('fetches the service items', async () => { mock = await inspectSearchParams((setup) => - getServicesItems({ setup, searchAggregatedTransactions: false }) + getServicesItems({ + setup, + searchAggregatedTransactions: false, + logger: {} as any, + }) ); const allParams = mock.spy.mock.calls.map((call) => call[0]); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 538ba3926c792..5673aa123b6aa 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -30,7 +30,11 @@ export const servicesRoute = createRoute(() => ({ setup ); - const services = await getServices({ setup, searchAggregatedTransactions }); + const services = await getServices({ + setup, + searchAggregatedTransactions, + logger: context.logger, + }); return services; }, diff --git a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts index b5f3ae834d5e2..608f8ff49ce5a 100644 --- a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts +++ b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* - * Support additional agent types by appending definitions in mappings.json - * (for telemetry) and the AgentName type. - */ -export type AgentName = +export type ElasticAgentName = | 'go' | 'java' | 'js-base' @@ -16,7 +12,9 @@ export type AgentName = | 'nodejs' | 'python' | 'dotnet' - | 'ruby' + | 'ruby'; + +export type OpenTelemetryAgentName = | 'otlp' | 'opentelemetry/cpp' | 'opentelemetry/dotnet' @@ -29,6 +27,12 @@ export type AgentName = | 'opentelemetry/ruby' | 'opentelemetry/webjs'; +/* + * Support additional agent types by appending definitions in mappings.json + * (for telemetry) and the AgentName type. + */ +export type AgentName = ElasticAgentName | OpenTelemetryAgentName; + export interface Agent { ephemeral_id?: string; name: AgentName; diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index 39a8262a5deec..a084e8fe3349e 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollector } from '../../types'; import { workpadCollector, workpadSchema, WorkpadTelemetry } from './workpad_collector'; @@ -37,7 +37,7 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index ffeecf27743f5..52e4a15a3f445 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -10,7 +10,7 @@ import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt } from './status'; -import { CaseConnectorRt, ESCaseConnector } from '../connectors'; +import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../connectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { ActionTypeExecutorResult } from '../../../../actions/server/types'; @@ -133,7 +133,7 @@ export const ServiceConnectorCommentParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); -export const ServiceConnectorCaseParamsRt = rt.type({ +export const ServiceConnectorBasicCaseParamsRt = rt.type({ comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), createdAt: rt.string, createdBy: ServiceConnectorUserParams, @@ -145,6 +145,11 @@ export const ServiceConnectorCaseParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); +export const ServiceConnectorCaseParamsRt = rt.intersection([ + ServiceConnectorBasicCaseParamsRt, + ConnectorPartialFieldsRt, +]); + export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ title: rt.string, diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts index 88d81eed2d87d..0019afe7c6b74 100644 --- a/x-pack/plugins/case/common/api/connectors/index.ts +++ b/x-pack/plugins/case/common/api/connectors/index.ts @@ -20,6 +20,12 @@ export const ConnectorFieldsRt = rt.union([ rt.null, ]); +export const ConnectorPartialFieldsRt = rt.partial({ + ...JiraFieldsRT.props, + ...ResilientFieldsRT.props, + ...ServiceNowFieldsRT.props, +}); + export enum ConnectorTypes { jira = '.jira', resilient = '.resilient', diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts index 7ec3888e4e1e4..6634ca630daf3 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts @@ -5,6 +5,7 @@ */ import { createCloudUsageCollector } from './cloud_usage_collector'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; const mockUsageCollection = () => ({ makeUsageCollector: jest.fn().mockImplementation((args: any) => ({ ...args })), @@ -25,9 +26,9 @@ describe('createCloudUsageCollector', () => { const mockConfigs = getMockConfigs(true); const usageCollection = mockUsageCollection() as any; const collector = createCloudUsageCollector(usageCollection, mockConfigs); - const callCluster = {} as any; // Sending any as the callCluster client because it's not needed in this collector but TS requires it when calling it. + const collectorFetchContext = createCollectorFetchContextMock(); - expect((await collector.fetch(callCluster)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited + expect((await collector.fetch(collectorFetchContext)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts index 7f6663a39eeb3..5b634fe4cf26c 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts @@ -82,7 +82,7 @@ describe('EQL search strategy', () => { describe('async functionality', () => { it('performs an eql client search with params when no ID is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request, requestOptions]] = mockEqlSearch.mock.calls; expect(request.index).toEqual('logstash-*'); @@ -92,7 +92,7 @@ describe('EQL search strategy', () => { it('retrieves the current request if an id is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { id: 'my-search-id' }); + await eqlSearch.search({ id: 'my-search-id' }, {}, mockContext).toPromise(); const [[requestParams]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); @@ -103,7 +103,7 @@ describe('EQL search strategy', () => { describe('arguments', () => { it('sends along async search options', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -116,7 +116,7 @@ describe('EQL search strategy', () => { it('sends along default search parameters', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -129,14 +129,20 @@ describe('EQL search strategy', () => { it('allows search parameters to be overridden', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { - options, - params: { - ...params, - wait_for_completion_timeout: '5ms', - keep_on_completion: false, - }, - }); + await eqlSearch + .search( + { + options, + params: { + ...params, + wait_for_completion_timeout: '5ms', + keep_on_completion: false, + }, + }, + {}, + mockContext + ) + .toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -150,10 +156,16 @@ describe('EQL search strategy', () => { it('allows search options to be overridden', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { - options: { ...options, maxRetries: 2, ignore: [300] }, - params, - }); + await eqlSearch + .search( + { + options: { ...options, maxRetries: 2, ignore: [300] }, + params, + }, + {}, + mockContext + ) + .toPromise(); const [[, requestOptions]] = mockEqlSearch.mock.calls; expect(requestOptions).toEqual( @@ -166,7 +178,9 @@ describe('EQL search strategy', () => { it('passes transport options for an existing request', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { id: 'my-search-id', options: { ignore: [400] } }); + await eqlSearch + .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockContext) + .toPromise(); const [[, requestOptions]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index 2516693a7f29b..a7ca999699e23 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import { Logger } from 'kibana/server'; import { ApiResponse, TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -26,48 +27,51 @@ export const eqlSearchStrategyProvider = ( id, }); }, - search: async (context, request, options) => { - logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); - let promise: TransportRequestPromise; - const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql; - const uiSettingsClient = await context.core.uiSettings.client; - const asyncOptions = getAsyncOptions(); - const searchOptions = toSnakeCase({ ...request.options }); + search: (request, options, context) => + from( + new Promise(async (resolve) => { + logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); + let promise: TransportRequestPromise; + const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql; + const uiSettingsClient = await context.core.uiSettings.client; + const asyncOptions = getAsyncOptions(); + const searchOptions = toSnakeCase({ ...request.options }); - if (request.id) { - promise = eqlClient.get( - { - id: request.id, - ...toSnakeCase(asyncOptions), - }, - searchOptions - ); - } else { - const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( - uiSettingsClient - ); - const searchParams = toSnakeCase({ - ignoreThrottled, - ignoreUnavailable, - ...asyncOptions, - ...request.params, - }); + if (request.id) { + promise = eqlClient.get( + { + id: request.id, + ...toSnakeCase(asyncOptions), + }, + searchOptions + ); + } else { + const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( + uiSettingsClient + ); + const searchParams = toSnakeCase({ + ignoreThrottled, + ignoreUnavailable, + ...asyncOptions, + ...request.params, + }); - promise = eqlClient.search( - searchParams as EqlSearchStrategyRequest['params'], - searchOptions - ); - } + promise = eqlClient.search( + searchParams as EqlSearchStrategyRequest['params'], + searchOptions + ); + } - const rawResponse = await shimAbortSignal(promise, options?.abortSignal); - const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body; + const rawResponse = await shimAbortSignal(promise, options?.abortSignal); + const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body; - return { - id, - isPartial, - isRunning, - rawResponse, - }; - }, + resolve({ + id, + isPartial, + isRunning, + rawResponse, + }); + }) + ), }; }; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index f4f3d894a4576..bab304b6afc9f 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -86,7 +86,9 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + await esSearch + .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; @@ -100,7 +102,9 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { id: 'foo', params }); + await esSearch + .search({ id: 'foo', params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockGetCaller).toBeCalled(); const request = mockGetCaller.mock.calls[0][0]; @@ -115,10 +119,16 @@ describe('ES search strategy', () => { const params = { index: 'foo-程', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { - indexType: 'rollup', - params, - }); + await esSearch + .search( + { + indexType: 'rollup', + params, + }, + {}, + (mockContext as unknown) as RequestHandlerContext + ) + .toPromise(); expect(mockApiCaller).toBeCalled(); const { method, path } = mockApiCaller.mock.calls[0][0]; @@ -132,7 +142,9 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + await esSearch + .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 7475228724388..9b89fb9fab3cb 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; @@ -36,35 +37,38 @@ export const enhancedEsSearchStrategyProvider = ( logger: Logger, usage?: SearchUsage ): ISearchStrategy => { - const search = async ( - context: RequestHandlerContext, + const search = ( request: IEnhancedEsSearchRequest, - options?: ISearchOptions - ) => { - logger.debug(`search ${JSON.stringify(request.params) || request.id}`); - - const isAsync = request.indexType !== 'rollup'; - - try { - const response = isAsync - ? await asyncSearch(context, request, options) - : await rollupSearch(context, request, options); - - if ( - usage && - isAsync && - isEnhancedEsSearchResponse(response) && - isCompleteResponse(response) - ) { - usage.trackSuccess(response.rawResponse.took); - } - - return response; - } catch (e) { - if (usage) usage.trackError(); - throw e; - } - }; + options: ISearchOptions, + context: RequestHandlerContext + ) => + from( + new Promise(async (resolve, reject) => { + logger.debug(`search ${JSON.stringify(request.params) || request.id}`); + + const isAsync = request.indexType !== 'rollup'; + + try { + const response = isAsync + ? await asyncSearch(request, options, context) + : await rollupSearch(request, options, context); + + if ( + usage && + isAsync && + isEnhancedEsSearchResponse(response) && + isCompleteResponse(response) + ) { + usage.trackSuccess(response.rawResponse.took); + } + + resolve(response); + } catch (e) { + if (usage) usage.trackError(); + reject(e); + } + }) + ); const cancel = async (context: RequestHandlerContext, id: string) => { logger.debug(`cancel ${id}`); @@ -74,9 +78,9 @@ export const enhancedEsSearchStrategyProvider = ( }; async function asyncSearch( - context: RequestHandlerContext, request: IEnhancedEsSearchRequest, - options?: ISearchOptions + options: ISearchOptions, + context: RequestHandlerContext ): Promise { let promise: TransportRequestPromise; const esClient = context.core.elasticsearch.client.asCurrentUser; @@ -112,9 +116,9 @@ export const enhancedEsSearchStrategyProvider = ( } const rollupSearch = async function ( - context: RequestHandlerContext, request: IEnhancedEsSearchRequest, - options?: ISearchOptions + options: ISearchOptions, + context: RequestHandlerContext ): Promise { const esClient = context.core.elasticsearch.client.asCurrentUser; const uiSettingsClient = await context.core.uiSettings.client; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index dfbe19ba21a94..953e6244b077f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -21,8 +21,10 @@ import { fatalErrorsServiceMock, } from '../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +import { CloudSetup } from '../../../cloud/public'; import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; +import { KibanaContextProvider } from '../../public/shared_imports'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; @@ -148,7 +150,14 @@ const save = (rendered: ReactWrapper) => { describe('edit policy', () => { beforeEach(() => { component = ( - + + + ); ({ http } = editPolicyHelpers.setup()); @@ -447,6 +456,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -495,6 +505,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -507,6 +518,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -519,6 +531,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -568,6 +581,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -626,6 +640,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -638,6 +653,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -650,6 +666,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -679,4 +696,104 @@ describe('edit policy', () => { expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); }); + describe('not on cloud', () => { + beforeEach(() => { + server.respondImmediately = true; + }); + test('should show all allocation options, even if using legacy config', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that only the custom and off options exist + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + }); + describe('on cloud', () => { + beforeEach(() => { + component = ( + + + + ); + ({ http } = editPolicyHelpers.setup()); + ({ server, httpRequestsMockHelpers } = http); + server.respondImmediately = true; + + httpRequestsMockHelpers.setPoliciesResponse(policies); + }); + + describe('with legacy data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that only the custom and off options exist + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('should show off, custom and data role options on cloud with data roles', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + + test('should show cloud notice when cold tier nodes do not exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'cold'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); + // Assert that other notices are not showing + expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + }); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index fcdbdf2c9cc90..ccdd7fcb11778 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -9,4 +9,12 @@ import { NodeDataRoleWithCatchAll } from '.'; export interface ListNodesRouteResponse { nodesByAttributes: { [attributePair: string]: string[] }; nodesByRoles: { [role in NodeDataRoleWithCatchAll]?: string[] }; + + /** + * A flag to indicate whether a node is using `settings.node.data` which is the now deprecated way cloud configured + * nodes to have data (and other) roles. + * + * If this is true, it means the cluster is using legacy cloud configuration for data allocation, not node roles. + */ + isUsingDeprecatedDataRoleConfig: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 479d651fc6698..1b0a73c6a0133 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -9,6 +9,7 @@ "features" ], "optionalPlugins": [ + "cloud", "usageCollection", "indexManagement", "home" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index d7812f186a03f..7a7fd20e96c63 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -8,6 +8,9 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; +import { CloudSetup } from '../../../cloud/public'; + +import { KibanaContextProvider } from '../shared_imports'; import { App } from './app'; @@ -16,11 +19,14 @@ export const renderApp = ( I18nContext: I18nStart['Context'], history: ScopedHistory, navigateToApp: ApplicationStart['navigateToApp'], - getUrlForApp: ApplicationStart['getUrlForApp'] + getUrlForApp: ApplicationStart['getUrlForApp'], + cloud?: CloudSetup ): UnmountCallback => { render( - + + + , element ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx new file mode 100644 index 0000000000000..2dff55ac10de1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), +}; + +export const CloudDataTierCallout: FunctionComponent = () => { + return ( + + {i18nTexts.body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx index 4ec488f95c94d..f58f36fc45a0c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; @@ -90,7 +90,25 @@ const i18nTexts = { }; export const DataTierAllocation: FunctionComponent = (props) => { - const { phaseData, setPhaseData, phase, hasNodeAttributes } = props; + const { phaseData, setPhaseData, phase, hasNodeAttributes, disableDataTierOption } = props; + + useEffect(() => { + if (disableDataTierOption && phaseData.dataTierAllocationType === 'default') { + /** + * @TODO + * This is a slight hack because we only know we should disable the "default" option further + * down the component tree (i.e., after the policy has been deserialized). + * + * We reset the value to "custom" if we deserialized to "default". + * + * It would be better if we had all the information we needed before deserializing and + * were able to handle this at the deserialization step instead of patching further down + * the component tree - this should be a future refactor. + */ + setPhaseData('dataTierAllocationType', 'custom'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -102,21 +120,40 @@ export const DataTierAllocation: FunctionComponent = (props) => { onChange={(value) => setPhaseData('dataTierAllocationType', value)} options={ [ + disableDataTierOption + ? undefined + : { + 'data-test-subj': 'defaultDataAllocationOption', + value: 'default', + inputDisplay: i18nTexts.allocationOptions[phase].default.input, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].default.input} + +

+ {i18nTexts.allocationOptions[phase].default.helpText} +

+
+ + ), + }, { - value: 'default', - inputDisplay: i18nTexts.allocationOptions[phase].default.input, + 'data-test-subj': 'customDataAllocationOption', + value: 'custom', + inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, dropdownDisplay: ( <> - {i18nTexts.allocationOptions[phase].default.input} + {i18nTexts.allocationOptions[phase].custom.inputDisplay}

- {i18nTexts.allocationOptions[phase].default.helpText} + {i18nTexts.allocationOptions[phase].custom.helpText}

), }, { + 'data-test-subj': 'noneDataAllocationOption', value: 'none', inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay, dropdownDisplay: ( @@ -130,22 +167,7 @@ export const DataTierAllocation: FunctionComponent = (props) => { ), }, - { - 'data-test-subj': 'customDataAllocationOption', - value: 'custom', - inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].custom.inputDisplay} - -

- {i18nTexts.allocationOptions[phase].custom.helpText} -

-
- - ), - }, - ] as SelectOptions[] + ].filter(Boolean) as SelectOptions[] } /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx index 8faa9bb2972c2..42f9e8494a0b3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { PhaseWithAllocation, NodeDataRole } from '../../../../../../common/types'; @@ -102,10 +102,5 @@ export const DefaultAllocationNotice: FunctionComponent = ({ phase, targe ); - return ( - <> - - {content} - - ); + return content; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts index dcbdf960fd380..937e3dd28da97 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts @@ -10,3 +10,4 @@ export { NodeAttrsDetails } from './node_attrs_details'; export { DataTierAllocation } from './data_tier_allocation'; export { DefaultAllocationNotice } from './default_allocation_notice'; export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx index ceccc51f95c1f..69185277f64ce 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { PhaseWithAllocation } from '../../../../../../common/types'; @@ -38,16 +38,13 @@ export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAlloca phase, }) => { return ( - <> - - - {i18nTexts[phase].body} - - + + {i18nTexts[phase].body} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts index d4cb31a3be9e7..d3dd536d97df0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts @@ -19,4 +19,10 @@ export interface SharedProps { isShowingErrors: boolean; nodes: ListNodesRouteResponse['nodesByAttributes']; hasNodeAttributes: boolean; + /** + * When on Cloud we want to disable the data tier allocation option when we detect that we are not + * using node roles in our Node config yet. See {@link ListNodesRouteResponse} for information about how this is + * detected. + */ + disableDataTierOption: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx index 623d443a1db01..b3772a6e3ebd4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx @@ -6,8 +6,9 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { useKibana } from '../../../../../shared_imports'; import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types'; import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; import { getAvailableNodeRoleForPhase } from '../../../../lib/data_tiers'; @@ -18,6 +19,7 @@ import { DefaultAllocationNotice, NoNodeAttributesWarning, NodesDataProvider, + CloudDataTierCallout, } from '../../components/data_tier_allocation'; const i18nTexts = { @@ -46,35 +48,61 @@ export const DataTierAllocationField: FunctionComponent = ({ isShowingErrors, errors, }) => { + const { + services: { cloud }, + } = useKibana(); + return ( - {(nodesData) => { - const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length); - - const renderDefaultAllocationNotice = () => { - if (phaseData.dataTierAllocationType !== 'default') { - return null; - } + {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { + const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); - const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesData.nodesByRoles); - if ( - allocationNodeRole !== 'none' && - isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { - return null; - } + const renderNotice = () => { + switch (phaseData.dataTierAllocationType) { + case 'default': + const isCloudEnabled = cloud?.isCloudEnabled ?? false; + const isUsingNodeRoles = !isUsingDeprecatedDataRoleConfig; + if ( + isCloudEnabled && + isUsingNodeRoles && + phase === 'cold' && + !nodesByRoles.data_cold?.length + ) { + // Tell cloud users they can deploy cold tier nodes. + return ( + <> + + + + ); + } - return ; - }; - - const renderNodeAttributesWarning = () => { - if (phaseData.dataTierAllocationType !== 'custom') { - return null; - } - if (hasNodeAttrs) { - return null; + const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); + if ( + allocationNodeRole === 'none' || + !isNodeRoleFirstPreference(phase, allocationNodeRole) + ) { + return ( + <> + + + + ); + } + break; + case 'custom': + if (!hasNodeAttrs) { + return ( + <> + + + + ); + } + break; + default: + return null; } - return ; }; return ( @@ -92,12 +120,14 @@ export const DataTierAllocationField: FunctionComponent = ({ setPhaseData={setPhaseData} phaseData={phaseData} isShowingErrors={isShowingErrors} - nodes={nodesData.nodesByAttributes} + nodes={nodesByAttributes} + disableDataTierOption={ + !!(isUsingDeprecatedDataRoleConfig && cloud?.isCloudEnabled) + } /> - {/* Data tier related warnings */} - {renderDefaultAllocationNotice()} - {renderNodeAttributesWarning()} + {/* Data tier related warnings and call-to-action notices */} + {renderNotice()} diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 645a78bfc99b8..24ce036c0e058 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -31,7 +31,7 @@ export class IndexLifecycleManagementPlugin { getStartServices, } = coreSetup; - const { usageCollection, management, indexManagement, home } = plugins; + const { usageCollection, management, indexManagement, home, cloud } = plugins; // Initialize services even if the app isn't mounted, because they're used by index management extensions. initHttp(http); @@ -65,7 +65,8 @@ export class IndexLifecycleManagementPlugin { I18nContext, history, navigateToApp, - getUrlForApp + getUrlForApp, + cloud ); return () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts new file mode 100644 index 0000000000000..d479b821ceefc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AppServicesContext } from './types'; +import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; + +export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 65db00f1e68c1..c9b9b063cd45f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -8,10 +8,12 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; +import { CloudSetup } from '../../cloud/public'; export interface PluginsDependencies { usageCollection?: UsageCollectionSetup; management: ManagementSetup; + cloud?: CloudSetup; indexManagement?: IndexManagementPluginSetup; home?: HomePublicPluginSetup; } @@ -21,3 +23,7 @@ export interface ClientConfigType { enabled: boolean; }; } + +export interface AppServicesContext { + cloud?: CloudSetup; +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts new file mode 100644 index 0000000000000..e547c3f662432 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts @@ -0,0 +1,2295 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * The fixtures below are from the "_nodes/settings" endpoint on a 7.9.2 Cloud-created cluster. + */ + +export const cloudNodeSettingsWithLegacy = { + _nodes: { + successful: 5, + failed: 0, + total: 5, + }, + cluster_name: '6ee9547c30214d278d2a63c4de98dea5', + nodes: { + t49k7mdeRIiELuOt_MOZ1g: { + transport_address: '10.47.32.43:19833', + name: 'instance-0000000002', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000002', + master: 'false', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18120', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18120', + }, + network: { + publish_host: '10.47.32.43', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19833', + }, + profiles: { + client: { + port: '20296', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.43', + host: '10.47.32.43', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + build_type: 'docker', + }, + 'SgaCpsXAQu-oTsP4iLGZWw': { + transport_address: '10.47.32.33:19227', + name: 'tiebreaker-0000000004', + roles: ['master', 'remote_cluster_client', 'voting_only'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + region: 'unknown-region', + transform: { + node: 'false', + }, + instance_configuration: 'gcp.master.1', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + ml: 'false', + ingest: 'false', + name: 'tiebreaker-0000000004', + master: 'true', + voting_only: 'true', + data: 'false', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18013', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18013', + }, + network: { + publish_host: '10.47.32.33', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19227', + }, + profiles: { + client: { + port: '20281', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.33', + host: '10.47.32.33', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + 'transform.node': 'false', + region: 'unknown-region', + instance_configuration: 'gcp.master.1', + 'xpack.installed': 'true', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + build_type: 'docker', + }, + 'ZVndRfrfSl-kmEyZgJu0JQ': { + transport_address: '10.47.47.205:19570', + name: 'instance-0000000001', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000001', + master: 'true', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18760', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18760', + }, + network: { + publish_host: '10.47.47.205', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19570', + }, + profiles: { + client: { + port: '20803', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.205', + host: '10.47.47.205', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + build_type: 'docker', + }, + Tx8Xig60SIuitXhY0srD6Q: { + transport_address: '10.47.32.41:19901', + name: 'instance-0000000003', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000003', + master: 'false', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18977', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18977', + }, + network: { + publish_host: '10.47.32.41', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19901', + }, + profiles: { + client: { + port: '20466', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.41', + host: '10.47.32.41', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + build_type: 'docker', + }, + Qtpmy7aBSIaOZisv9Q92TA: { + transport_address: '10.47.47.203:19498', + name: 'instance-0000000000', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000000', + master: 'true', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18221', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18221', + }, + network: { + publish_host: '10.47.47.203', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19498', + }, + profiles: { + client: { + port: '20535', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.203', + host: '10.47.47.203', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + build_type: 'docker', + }, + }, +}; + +export const cloudNodeSettingsWithoutLegacy = { + _nodes: { + successful: 5, + failed: 0, + total: 5, + }, + cluster_name: '6ee9547c30214d278d2a63c4de98dea5', + nodes: { + t49k7mdeRIiELuOt_MOZ1g: { + transport_address: '10.47.32.43:19833', + name: 'instance-0000000002', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000002', + master: 'false', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18120', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18120', + }, + network: { + publish_host: '10.47.32.43', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19833', + }, + profiles: { + client: { + port: '20296', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.43', + host: '10.47.32.43', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + build_type: 'docker', + }, + 'SgaCpsXAQu-oTsP4iLGZWw': { + transport_address: '10.47.32.33:19227', + name: 'tiebreaker-0000000004', + roles: ['master', 'remote_cluster_client', 'voting_only'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + region: 'unknown-region', + transform: { + node: 'false', + }, + instance_configuration: 'gcp.master.1', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + ml: 'false', + ingest: 'false', + name: 'tiebreaker-0000000004', + master: 'true', + voting_only: 'true', + data: 'false', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18013', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18013', + }, + network: { + publish_host: '10.47.32.33', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19227', + }, + profiles: { + client: { + port: '20281', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.33', + host: '10.47.32.33', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + 'transform.node': 'false', + region: 'unknown-region', + instance_configuration: 'gcp.master.1', + 'xpack.installed': 'true', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + build_type: 'docker', + }, + 'ZVndRfrfSl-kmEyZgJu0JQ': { + transport_address: '10.47.47.205:19570', + name: 'instance-0000000001', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000001', + master: 'true', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18760', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18760', + }, + network: { + publish_host: '10.47.47.205', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19570', + }, + profiles: { + client: { + port: '20803', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.205', + host: '10.47.47.205', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + build_type: 'docker', + }, + Tx8Xig60SIuitXhY0srD6Q: { + transport_address: '10.47.32.41:19901', + name: 'instance-0000000003', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000003', + master: 'false', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18977', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18977', + }, + network: { + publish_host: '10.47.32.41', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19901', + }, + profiles: { + client: { + port: '20466', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.41', + host: '10.47.32.41', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + build_type: 'docker', + }, + Qtpmy7aBSIaOZisv9Q92TA: { + transport_address: '10.47.47.203:19498', + name: 'instance-0000000000', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000000', + master: 'true', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18221', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18221', + }, + network: { + publish_host: '10.47.47.203', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19498', + }, + profiles: { + client: { + port: '20535', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.203', + host: '10.47.47.203', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + build_type: 'docker', + }, + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts new file mode 100644 index 0000000000000..5adb7763074fe --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { convertSettingsIntoLists } from '../register_list_route'; +import { cloudNodeSettingsWithLegacy, cloudNodeSettingsWithoutLegacy } from './fixtures'; + +describe('convertSettingsIntoLists', () => { + it('detects node role config', () => { + const result = convertSettingsIntoLists(cloudNodeSettingsWithoutLegacy, []); + expect(result.isUsingDeprecatedDataRoleConfig).toBe(false); + }); + + it('converts cloud settings into the expected response and detects deprecated config', () => { + const result = convertSettingsIntoLists(cloudNodeSettingsWithLegacy, []); + + expect(result.isUsingDeprecatedDataRoleConfig).toBe(true); + expect(result.nodesByRoles).toEqual({ + data: [ + 't49k7mdeRIiELuOt_MOZ1g', + 'ZVndRfrfSl-kmEyZgJu0JQ', + 'Tx8Xig60SIuitXhY0srD6Q', + 'Qtpmy7aBSIaOZisv9Q92TA', + ], + }); + expect(result.nodesByAttributes).toMatchInlineSnapshot(` + Object { + "availability_zone:europe-west4-a": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "availability_zone:europe-west4-b": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "availability_zone:europe-west4-c": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "data:hot": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "data:warm": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "instance_configuration:gcp.data.highio.1": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "instance_configuration:gcp.data.highstorage.1": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "instance_configuration:gcp.master.1": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "logical_availability_zone:tiebreaker": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "logical_availability_zone:zone-0": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "logical_availability_zone:zone-1": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "region:unknown-region": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "server_name:instance-0000000000.6ee9547c30214d278d2a63c4de98dea5": Array [ + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "server_name:instance-0000000001.6ee9547c30214d278d2a63c4de98dea5": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + ], + "server_name:instance-0000000002.6ee9547c30214d278d2a63c4de98dea5": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + ], + "server_name:instance-0000000003.6ee9547c30214d278d2a63c4de98dea5": Array [ + "Tx8Xig60SIuitXhY0srD6Q", + ], + "server_name:tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "transform.node:false": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "transform.node:true": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "xpack.installed:true": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + } + `); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index f7f048e809d75..bb1679e695e14 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -9,22 +9,27 @@ import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -interface Stats { +interface Settings { nodes: { [nodeId: string]: { attributes: Record; roles: string[]; + settings: { + node: { + data?: string; + }; + }; }; }; } -function convertStatsIntoList( - stats: Stats, +export function convertSettingsIntoLists( + settings: Settings, disallowedNodeAttributes: string[] ): ListNodesRouteResponse { - return Object.entries(stats.nodes).reduce( - (accum, [nodeId, nodeStats]) => { - const attributes = nodeStats.attributes || {}; + return Object.entries(settings.nodes).reduce( + (accum, [nodeId, nodeSettings]) => { + const attributes = nodeSettings.attributes || {}; for (const [key, value] of Object.entries(attributes)) { const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); if (isNodeAttributeAllowed) { @@ -34,14 +39,26 @@ function convertStatsIntoList( } } - const dataRoles = nodeStats.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; + const dataRoles = nodeSettings.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; for (const role of dataRoles) { accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? []; accum.nodesByRoles[role as NodeDataRole]!.push(nodeId); } + + // If we detect a single node using legacy "data:true" setting we know we are not using data roles for + // data allocation. + if (nodeSettings.settings?.node?.data === 'true') { + accum.isUsingDeprecatedDataRoleConfig = true; + } + return accum; }, - { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse + { + nodesByAttributes: {}, + nodesByRoles: {}, + // Start with assumption that we are not using deprecated config + isUsingDeprecatedDataRoleConfig: false, + } as ListNodesRouteResponse ); } @@ -64,11 +81,17 @@ export function registerListRoute({ router, config, license }: RouteDependencies { path: addBasePath('/nodes/list'), validate: false }, license.guardApiRoute(async (context, request, response) => { try { - const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats< - Stats - >(); - const body: ListNodesRouteResponse = convertStatsIntoList( - statsResponse.body, + const settingsResponse = await context.core.elasticsearch.client.asCurrentUser.transport.request( + { + method: 'GET', + path: '/_nodes/settings', + querystring: { + format: 'json', + }, + } + ); + const body: ListNodesRouteResponse = convertSettingsIntoLists( + settingsResponse.body as Settings, disallowedNodeAttributes ); return response.ok({ body }); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 6fbe177d24e06..22e6f09907d75 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -8,7 +8,7 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { CoreStart } from '../../../../../src/core/public'; +import { CoreSetup, CoreStart } from '../../../../../src/core/public'; import { IngestManagerSetup } from '../../../ingest_manager/public'; import { IndexMgmtMetricsType } from '../types'; @@ -34,6 +34,7 @@ export interface AppDependencies { }; history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + uiSettings: CoreSetup['uiSettings']; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index b3bf071948956..c47ea4a884111 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -73,6 +73,10 @@ export * from './meta_parameter'; export * from './ignore_above_parameter'; +export { RuntimeTypeParameter } from './runtime_type_parameter'; + +export { PainlessScriptParameter } from './painless_script_parameter'; + export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx new file mode 100644 index 0000000000000..19746034b530c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui'; + +import { CodeEditor, UseField } from '../../../shared_imports'; +import { getFieldConfig } from '../../../lib'; +import { EditFieldFormRow } from '../fields/edit_field'; + +interface Props { + stack?: boolean; +} + +export const PainlessScriptParameter = ({ stack }: Props) => { + return ( + + {(scriptField) => { + const error = scriptField.getErrorsMessages(); + const isInvalid = error ? Boolean(error.length) : false; + + const field = ( + + + + ); + + const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.painlessScript.title', { + defaultMessage: 'Emitted value', + }); + + const fieldDescription = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.painlessScript.description', + { + defaultMessage: 'Use emit() to define the value of this runtime field.', + } + ); + + if (stack) { + return ( + + {field} + + ); + } + + return ( + {fieldTitle}} + description={fieldDescription} + fullWidth={true} + > + {field} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx index 6575fe1fac7b8..62810df44b5af 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx @@ -93,17 +93,17 @@ export const PathParameter = ({ field, allFields }: Props) => { <> {!Boolean(suggestedFields.length) && ( <> - -

- {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.aliasType.noFieldsAddedWarningMessage', - { - defaultMessage: - 'You need to add at least one field before creating an alias.', - } - )} -

-
+ )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx new file mode 100644 index 0000000000000..4bdb15af5e7d9 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiDescribedFormGroup, + EuiSpacer, +} from '@elastic/eui'; + +import { UseField } from '../../../shared_imports'; +import { DataType } from '../../../types'; +import { getFieldConfig } from '../../../lib'; +import { RUNTIME_FIELD_OPTIONS, TYPE_DEFINITION } from '../../../constants'; +import { EditFieldFormRow, FieldDescriptionSection } from '../fields/edit_field'; + +interface Props { + stack?: boolean; +} + +export const RuntimeTypeParameter = ({ stack }: Props) => { + return ( + + {(runtimeTypeField) => { + const { label, value, setValue } = runtimeTypeField; + const typeDefinition = + TYPE_DEFINITION[(value as EuiComboBoxOptionOption[])[0]!.value as DataType]; + + const field = ( + <> + + + + + + + {/* Field description */} + {typeDefinition && ( + + {typeDefinition.description?.() as JSX.Element} + + )} + + ); + + const fieldTitle = i18n.translate('xpack.idxMgmt.mappingsEditor.runtimeType.title', { + defaultMessage: 'Emitted type', + }); + + const fieldDescription = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.runtimeType.description', + { + defaultMessage: 'Select the type of value emitted by the runtime field.', + } + ); + + if (stack) { + return ( + + {field} + + ); + } + + return ( + {fieldTitle}} + description={fieldDescription} + fullWidth={true} + > + {field} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx index 6752bb6d6af2b..69d56032eed6a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx @@ -56,14 +56,17 @@ export const TermVectorParameter = ({ field, defaultToggleValue }: Props) => { {formData.term_vector === 'with_positions_offsets' && ( <> - -

- {i18n.translate('xpack.idxMgmt.mappingsEditor.termVectorFieldWarningMessage', { + - + } + )} + /> )} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index a8170b1d59fbb..cff2b9af4fd10 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -14,15 +14,17 @@ import { EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector, + EuiSpacer, } from '@elastic/eui'; import { useForm, Form, FormDataProvider } from '../../../../shared_imports'; -import { EUI_SIZE } from '../../../../constants'; +import { EUI_SIZE, TYPE_DEFINITION } from '../../../../constants'; import { useDispatch } from '../../../../mappings_state_context'; import { fieldSerializer } from '../../../../lib'; -import { Field, NormalizedFields } from '../../../../types'; +import { Field, NormalizedFields, MainType } from '../../../../types'; import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters'; -import { getParametersFormForType } from './required_parameters_forms'; +import { FieldBetaBadge } from '../field_beta_badge'; +import { getRequiredParametersFormForType } from './required_parameters_forms'; const formWrapper = (props: any) =>

; @@ -195,18 +197,27 @@ export const CreateField = React.memo(function CreateFieldComponent({ {({ type, subType }) => { - const ParametersForm = getParametersFormForType( + const RequiredParametersForm = getRequiredParametersFormForType( type?.[0].value, subType?.[0].value ); - if (!ParametersForm) { + if (!RequiredParametersForm) { return null; } + const typeDefinition = TYPE_DEFINITION[type?.[0].value as MainType]; + return (
- + {typeDefinition.isBeta ? ( + <> + + + + ) : null} + +
); }} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts index 1112bf99713ed..5c04b2fbb336c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts @@ -11,6 +11,7 @@ import { AliasTypeRequiredParameters } from './alias_type'; import { TokenCountTypeRequiredParameters } from './token_count_type'; import { ScaledFloatTypeRequiredParameters } from './scaled_float_type'; import { DenseVectorRequiredParameters } from './dense_vector_type'; +import { RuntimeTypeRequiredParameters } from './runtime_type'; export interface ComponentProps { allFields: NormalizedFields['byId']; @@ -21,9 +22,10 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { token_count: TokenCountTypeRequiredParameters, scaled_float: ScaledFloatTypeRequiredParameters, dense_vector: DenseVectorRequiredParameters, + runtime: RuntimeTypeRequiredParameters, }; -export const getParametersFormForType = ( +export const getRequiredParametersFormForType = ( type: MainType, subType?: SubType ): ComponentType | undefined => diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx new file mode 100644 index 0000000000000..54907295f8a15 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/runtime_type.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { RuntimeTypeParameter, PainlessScriptParameter } from '../../../field_parameters'; + +export const RuntimeTypeRequiredParameters = () => { + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index 3b55c5ac076c2..e91a666cc4221 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -5,12 +5,13 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FormDataProvider } from '../../../../shared_imports'; -import { MainType, SubType, Field } from '../../../../types'; +import { MainType, SubType, Field, DataTypeDefinition } from '../../../../types'; import { TYPE_DEFINITION } from '../../../../constants'; import { NameParameter, TypeParameter, SubTypeParameter } from '../../field_parameters'; +import { FieldBetaBadge } from '../field_beta_badge'; import { FieldDescriptionSection } from './field_description_section'; interface Props { @@ -19,6 +20,25 @@ interface Props { isMultiField: boolean; } +const getTypeDefinition = (type: MainType, subType: SubType): DataTypeDefinition | undefined => { + if (!type) { + return; + } + + const typeDefinition = TYPE_DEFINITION[type]; + const hasSubType = typeDefinition.subTypes !== undefined; + + if (hasSubType) { + if (subType) { + return TYPE_DEFINITION[subType]; + } + + return; + } + + return typeDefinition; +}; + export const EditFieldHeaderForm = React.memo( ({ defaultValue, isRootLevelField, isMultiField }: Props) => { return ( @@ -56,27 +76,29 @@ export const EditFieldHeaderForm = React.memo( {/* Field description */} - - - {({ type, subType }) => { - if (!type) { - return null; - } - const typeDefinition = TYPE_DEFINITION[type[0].value as MainType]; - const hasSubType = typeDefinition.subTypes !== undefined; - - if (hasSubType) { - if (subType) { - const subTypeDefinition = TYPE_DEFINITION[subType as SubType]; - return (subTypeDefinition?.description?.() as JSX.Element) ?? null; - } - return null; - } + + {({ type, subType }) => { + const typeDefinition = getTypeDefinition( + type[0].value as MainType, + subType && (subType[0].value as SubType) + ); + const description = (typeDefinition?.description?.() as JSX.Element) ?? null; - return typeDefinition.description?.() as JSX.Element; - }} - - + return ( + <> + + + {typeDefinition?.isBeta && } + + + + + {description} + + + ); + }} +
); } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx index 2040d7f40d5cb..2301f7a47bf2f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; interface Props { @@ -19,7 +19,6 @@ export const FieldDescriptionSection = ({ children, isMultiField }: Props) => { return (
- {children} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_beta_badge.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_beta_badge.tsx new file mode 100644 index 0000000000000..99c725e8a00b3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_beta_badge.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBetaBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const FieldBetaBadge = () => { + const betaText = i18n.translate('xpack.idxMgmt.mappingsEditor.fieldBetaBadgeLabel', { + defaultMessage: 'Beta', + }); + + const tooltipText = i18n.translate('xpack.idxMgmt.mappingsEditor.fieldBetaBadgeTooltip', { + defaultMessage: 'This field type is not GA. Please help us by reporting any bugs.', + }); + + return ; +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 0f9308aa43448..d135d1b81419c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -31,6 +31,7 @@ import { JoinType } from './join_type'; import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; +import { RuntimeType } from './runtime_type'; import { WildcardType } from './wildcard_type'; import { PointType } from './point_type'; import { VersionType } from './version_type'; @@ -61,6 +62,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { histogram: HistogramType, constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, + runtime: RuntimeType, wildcard: WildcardType, point: PointType, version: VersionType, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx new file mode 100644 index 0000000000000..dcf5a74e0e304 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/runtime_type.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { RuntimeTypeParameter, PainlessScriptParameter } from '../../field_parameters'; +import { BasicParametersSection } from '../edit_field'; + +export const RuntimeType = () => { + return ( + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 4ab0ea0fb355b..1939f09fa6762 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { NormalizedField, NormalizedFields } from '../../../types'; -import { getTypeLabelFromType } from '../../../lib'; +import { getTypeLabelFromField } from '../../../lib'; import { CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER } from '../../../constants'; import { FieldsList } from './fields_list'; @@ -67,6 +67,7 @@ function FieldListItemComponent( isExpanded, path, } = field; + // When there aren't any "child" fields (the maxNestedDepth === 0), there is no toggle icon on the left of any field. // For that reason, we need to compensate and substract some indent to left align on the page. const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0; @@ -280,10 +281,10 @@ function FieldListItemComponent( ? i18n.translate('xpack.idxMgmt.mappingsEditor.multiFieldBadgeLabel', { defaultMessage: '{dataType} multi-field', values: { - dataType: getTypeLabelFromType(source.type), + dataType: getTypeLabelFromField(source), }, }) - : getTypeLabelFromType(source.type)} + : getTypeLabelFromField(source)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx index a2d9a50f28394..a4cc4b4776e2b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { SearchResult } from '../../../types'; import { TYPE_DEFINITION } from '../../../constants'; import { useDispatch } from '../../../mappings_state_context'; -import { getTypeLabelFromType } from '../../../lib'; +import { getTypeLabelFromField } from '../../../lib'; import { DeleteFieldProvider } from '../fields/delete_field_provider'; interface Props { @@ -121,7 +121,7 @@ export const SearchResultItem = React.memo(function FieldListItemFlatComponent({ dataType: TYPE_DEFINITION[source.type].label, }, }) - : getTypeLabelFromType(source.type)} + : getTypeLabelFromField(source)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 66be208fbb66b..07ca0a69afefb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -13,6 +13,25 @@ import { documentationService } from '../../../services/documentation'; import { MainType, SubType, DataType, DataTypeDefinition } from '../types'; export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { + runtime: { + value: 'runtime', + isBeta: true, + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.runtimeFieldDescription', { + defaultMessage: 'Runtime', + }), + // TODO: Add this once the page exists. + // documentation: { + // main: '/runtime_field.html', + // }, + description: () => ( +

+ +

+ ), + }, text: { value: 'text', label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.textDescription', { @@ -925,6 +944,7 @@ export const MAIN_TYPES: MainType[] = [ 'range', 'rank_feature', 'rank_features', + 'runtime', 'search_as_you_type', 'shape', 'text', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx index d16bf68b80e5d..25fdac5089b86 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx @@ -18,6 +18,7 @@ export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [ 'object', 'nested', 'alias', + 'runtime', ]; export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map( @@ -27,6 +28,35 @@ export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map }) ) as ComboBoxOption[]; +export const RUNTIME_FIELD_OPTIONS = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +] as ComboBoxOption[]; + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; + interface SuperSelectOptionConfig { inputDisplay: string; dropdownDisplay: JSX.Element; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index 281b14a25fcb6..1434b7d4b4429 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -20,6 +20,7 @@ import { import { AliasOption, DataType, + RuntimeType, ComboBoxOption, ParameterName, ParameterDefinition, @@ -27,6 +28,7 @@ import { import { documentationService } from '../../../services/documentation'; import { INDEX_DEFAULT } from './default_values'; import { TYPE_DEFINITION } from './data_types_definition'; +import { RUNTIME_FIELD_OPTIONS } from './field_options'; const { toInt } = fieldFormatters; const { emptyField, containsCharsField, numberGreaterThanField, isJsonField } = fieldValidators; @@ -185,6 +187,52 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.string, }, + runtime_type: { + fieldConfig: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: 'keyword', + deserializer: (fieldType: RuntimeType | undefined) => { + if (typeof fieldType === 'string' && Boolean(fieldType)) { + const label = + RUNTIME_FIELD_OPTIONS.find(({ value }) => value === fieldType)?.label ?? fieldType; + return [ + { + label, + value: fieldType, + }, + ]; + } + return []; + }, + serializer: (value: ComboBoxOption[]) => (value.length === 0 ? '' : value[0].value), + }, + schema: t.string, + }, + script: { + fieldConfig: { + defaultValue: '', + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.painlessScriptLabel', { + defaultMessage: 'Script', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.scriptIsRequiredErrorMessage', + { + defaultMessage: 'Script must emit() a value.', + } + ) + ), + }, + ], + }, + schema: t.string, + }, store: { fieldConfig: { type: FIELD_TYPES.CHECKBOX, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts index 8cd1bbf0764ab..0a59cafdcef47 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts @@ -4,7 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './utils'; +export { + getUniqueId, + getChildFieldsName, + getFieldMeta, + getTypeLabelFromField, + getFieldConfig, + getTypeMetaFromSource, + normalize, + deNormalize, + updateFieldsPathAfterFieldNameChange, + getAllChildFields, + getAllDescendantAliases, + getFieldAncestors, + filterTypesForMultiField, + filterTypesForNonRootFields, + getMaxNestedDepth, + buildFieldTreeFromIds, + shouldDeleteChildFieldsAfterTypeChange, + canUseMappingsEditor, + stripUndefinedValues, +} from './utils'; export * from './serializers'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index bc495b05e07b7..e1988c071314e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); +jest.mock('../constants', () => { + const { TYPE_DEFINITION } = jest.requireActual('../constants'); + return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION }; +}); -import { stripUndefinedValues } from './utils'; +import { stripUndefinedValues, getTypeLabelFromField } from './utils'; describe('utils', () => { describe('stripUndefinedValues()', () => { @@ -53,4 +56,46 @@ describe('utils', () => { expect(stripUndefinedValues(dataIN)).toEqual(dataOUT); }); }); + + describe('getTypeLabelFromField()', () => { + test('returns an unprocessed label for non-runtime fields', () => { + expect( + getTypeLabelFromField({ + name: 'testField', + type: 'keyword', + }) + ).toBe('Keyword'); + }); + + test(`returns a label prepended with 'Other' for unrecognized fields`, () => { + expect( + getTypeLabelFromField({ + name: 'testField', + // @ts-ignore + type: 'hyperdrive', + }) + ).toBe('Other: hyperdrive'); + }); + + test("returns a label prepended with 'Runtime' for runtime fields", () => { + expect( + getTypeLabelFromField({ + name: 'testField', + type: 'runtime', + runtime_type: 'keyword', + }) + ).toBe('Runtime Keyword'); + }); + + test("returns a label prepended with 'Runtime Other' for unrecognized runtime fields", () => { + expect( + getTypeLabelFromField({ + name: 'testField', + type: 'runtime', + // @ts-ignore + runtime_type: 'hyperdrive', + }) + ).toBe('Runtime Other: hyperdrive'); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index cbbef8700783c..fd7aa41638505 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -71,8 +71,23 @@ export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => }; }; -export const getTypeLabelFromType = (type: DataType) => - TYPE_DEFINITION[type] ? TYPE_DEFINITION[type].label : `${TYPE_DEFINITION.other.label}: ${type}`; +const getTypeLabel = (type?: DataType): string => { + return type && TYPE_DEFINITION[type] + ? TYPE_DEFINITION[type].label + : `${TYPE_DEFINITION.other.label}: ${type}`; +}; + +export const getTypeLabelFromField = (field: Field) => { + const { type, runtime_type: runtimeType } = field; + const typeLabel = getTypeLabel(type); + + if (type === 'runtime') { + const runtimeTypeLabel = getTypeLabel(runtimeType); + return `${typeLabel} ${runtimeTypeLabel}`; + } + + return typeLabel; +}; export const getFieldConfig = ( param: ParameterName, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 097d039527950..54b2486108183 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -51,3 +51,5 @@ export { OnJsonEditorUpdateHandler, GlobalFlyout, } from '../../../../../../../src/plugins/es_ui_shared/public'; + +export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index b1d2915ebb1aa..ee4dd55a5801f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -8,7 +8,7 @@ import { ReactNode } from 'react'; import { GenericObject } from './mappings_editor'; import { FieldConfig } from '../shared_imports'; -import { PARAMETERS_DEFINITION } from '../constants'; +import { PARAMETERS_DEFINITION, RUNTIME_FIELD_TYPES } from '../constants'; export interface DataTypeDefinition { label: string; @@ -19,6 +19,7 @@ export interface DataTypeDefinition { }; subTypes?: { label: string; types: SubType[] }; description?: () => ReactNode; + isBeta?: boolean; } export interface ParameterDefinition { @@ -35,6 +36,7 @@ export interface ParameterDefinition { } export type MainType = + | 'runtime' | 'text' | 'keyword' | 'numeric' @@ -74,6 +76,8 @@ export type SubType = NumericType | RangeType; export type DataType = MainType | SubType; +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + export type NumericType = | 'long' | 'integer' @@ -152,6 +156,8 @@ export type ParameterName = | 'depth_limit' | 'relations' | 'max_shingle_size' + | 'runtime_type' + | 'script' | 'value' | 'meta'; @@ -169,6 +175,7 @@ export interface Fields { interface FieldBasic { name: string; type: DataType; + runtime_type?: DataType; subType?: SubType; properties?: { [key: string]: Omit }; fields?: { [key: string]: Omit }; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index f881c2e01cefc..d8b5da8361c43 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -11,7 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { CoreStart } from '../../../../../src/core/public'; import { API_BASE_PATH } from '../../common'; -import { GlobalFlyout } from '../shared_imports'; +import { createKibanaReactContext, GlobalFlyout } from '../shared_imports'; import { AppContextProvider, AppDependencies } from './app_context'; import { App } from './app'; @@ -30,7 +30,12 @@ export const renderApp = ( const { i18n, docLinks, notifications, application } = core; const { Context: I18nContext } = i18n; - const { services, history, setBreadcrumbs } = dependencies; + const { services, history, setBreadcrumbs, uiSettings } = dependencies; + + // uiSettings is required by the CodeEditor component used to edit runtime field Painless scripts. + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + }); const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, @@ -44,17 +49,19 @@ export const renderApp = ( render( - - - - - - - - - - - + + + + + + + + + + + + + , elem ); diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 6257b68430cf0..f7b728c875762 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -41,6 +41,7 @@ export async function mountManagementSection( fatalErrors, application, chrome: { docTitle }, + uiSettings, } = core; docTitle.change(PLUGIN.getI18nName(i18n)); @@ -60,6 +61,7 @@ export async function mountManagementSection( services, history, setBreadcrumbs, + uiSettings, }; const unmountAppCallback = renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index d58545768732e..acb3eb512e2c1 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -44,4 +44,7 @@ export { export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string'; -export { reactRouterNavigate } from '../../../../src/plugins/kibana_react/public'; +export { + createKibanaReactContext, + reactRouterNavigate, +} from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/ingest_manager/common/openapi/README.md b/x-pack/plugins/ingest_manager/common/openapi/README.md new file mode 100644 index 0000000000000..72204d483b120 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/README.md @@ -0,0 +1,14 @@ +## The `openapi` folder + +* `entrypoint.yaml` is the overview file which links to the various files on disk. +* `bundled.{yaml,json}` is the resolved output of that entry & other files in a single file. It's currently generated with: + + ``` + npx swagger-cli bundle -o bundled.json -t json entrypoint.yaml + npx swagger-cli bundle -o bundled.yaml -t yaml entrypoint.yaml + ``` +* [Paths](paths/README.md): this defines each endpoint. A path can have one operation per http method. +* [Components](components/README.md): Reusable components like [`schemas`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject), + [`responses`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) + [`parameters`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject), etc + \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/openapi/bundled.json b/x-pack/plugins/ingest_manager/common/openapi/bundled.json new file mode 100644 index 0000000000000..1d00855de8935 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/bundled.json @@ -0,0 +1,2088 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Ingest Manager", + "version": "0.2", + "contact": { + "name": "Ingest Team" + }, + "license": { + "name": "Elastic" + } + }, + "servers": [ + { + "url": "http://localhost:5601/api/fleet", + "description": "local" + } + ], + "paths": { + "/agent_policies": { + "get": { + "summary": "Agent policy - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "items", + "total", + "page", + "perPage" + ] + } + } + } + } + }, + "operationId": "agent-policy-list", + "parameters": [ + { + "$ref": "#/components/parameters/page_size" + }, + { + "$ref": "#/components/parameters/page_index" + }, + { + "$ref": "#/components/parameters/kuery" + } + ], + "description": "" + }, + "post": { + "summary": "Agent policy - Create", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + } + } + } + } + } + }, + "operationId": "post-agent-policy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_agent_policy" + } + } + } + }, + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agent_policies/{agentPolicyId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentPolicyId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Agent policy - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "agent-policy-info", + "description": "Get one agent policy", + "parameters": [] + }, + "put": { + "summary": "Agent policy - Update", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "put-agent-policy-agentPolicyId", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_agent_policy" + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agent_policies/{agentPolicyId}/copy": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentPolicyId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Agent policy - copy one policy", + "operationId": "agent-policy-copy", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + }, + "description": "" + }, + "description": "Copies one agent policy" + } + }, + "/agent_policies/delete": { + "post": { + "summary": "Agent policy - Delete", + "operationId": "post-agent-policy-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ] + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentPolicyIds": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "parameters": [] + }, + "/agent-status": { + "get": { + "summary": "Fleet - Agent - Status for policy", + "tags": [], + "responses": {}, + "operationId": "get-fleet-agent-status", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "policyId", + "in": "query", + "required": false + } + ] + } + }, + "/agents": { + "get": { + "summary": "Fleet - Agent - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "list", + "total", + "page", + "perPage" + ] + } + } + } + } + }, + "operationId": "get-fleet-agents", + "security": [ + { + "basicAuth": [] + } + ] + } + }, + "/agents/{agentId}/acks": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Acks", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "acks" + ] + } + }, + "required": [ + "action" + ] + } + } + } + } + }, + "operationId": "post-fleet-agents-agentId-acks", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + }, + "/agents/{agentId}/checkin": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Check In", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "checkin" + ] + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "data": { + "type": "object" + }, + "id": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + } + }, + "required": [ + "agent_id", + "data", + "id", + "created_at", + "type" + ] + } + } + } + } + } + } + } + }, + "operationId": "post-fleet-agents-agentId-checkin", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "security": [ + { + "Access API Key": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "local_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/new_agent_event" + } + } + } + } + } + } + } + } + }, + "/agents/{agentId}/events": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Agent - Events", + "tags": [], + "responses": {}, + "operationId": "get-fleet-agents-agentId-events" + } + }, + "/agents/{agentId}/unenroll": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Unenroll", + "tags": [], + "responses": {}, + "operationId": "post-fleet-agents-unenroll", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/agents/{agentId}/upgrade": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Upgrade", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + }, + "400": { + "description": "BAD REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + }, + "operationId": "post-fleet-agents-upgrade", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + } + }, + "/agents/bulk_upgrade": { + "post": { + "summary": "Fleet - Agent - Bulk Upgrade", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_upgrade_agents" + } + } + } + }, + "400": { + "description": "BAD REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + }, + "operationId": "post-fleet-agents-bulk-upgrade", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_upgrade_agents" + } + } + } + } + } + }, + "/agents/enroll": { + "post": { + "summary": "Fleet - Agent - Enroll", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "item": { + "$ref": "#/components/schemas/agent" + } + } + } + } + } + } + }, + "operationId": "post-fleet-agents-enroll", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "PERMANENT", + "EPHEMERAL", + "TEMPORARY" + ] + }, + "shared_id": { + "type": "string" + }, + "metadata": { + "type": "object", + "required": [ + "local", + "user_provided" + ], + "properties": { + "local": { + "$ref": "#/components/schemas/agent_metadata" + }, + "user_provided": { + "$ref": "#/components/schemas/agent_metadata" + } + } + } + }, + "required": [ + "type", + "metadata" + ] + } + } + } + }, + "security": [ + { + "Enrollment API Key": [] + } + ] + } + }, + "/agents/setup": { + "get": { + "summary": "Agents setup - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + }, + "required": [ + "isInitialized" + ] + } + } + } + } + }, + "operationId": "get-agents-setup", + "security": [ + { + "basicAuth": [] + } + ] + }, + "post": { + "summary": "Agents setup - Create", + "operationId": "post-agents-setup", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + }, + "required": [ + "isInitialized" + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "admin_username": { + "type": "string" + }, + "admin_password": { + "type": "string" + } + }, + "required": [ + "admin_username", + "admin_password" + ] + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment-api-keys": { + "get": { + "summary": "Enrollment - List", + "tags": [], + "responses": {}, + "operationId": "get-fleet-enrollment-api-keys", + "parameters": [] + }, + "post": { + "summary": "Enrollment - Create", + "tags": [], + "responses": {}, + "operationId": "post-fleet-enrollment-api-keys", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment-api-keys/{keyId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "keyId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Enrollment - Info", + "tags": [], + "responses": {}, + "operationId": "get-fleet-enrollment-api-keys-keyId" + }, + "delete": { + "summary": "Enrollment - Delete", + "tags": [], + "responses": {}, + "operationId": "delete-fleet-enrollment-api-keys-keyId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/epm/categories": { + "get": { + "summary": "EPM - Categories", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "count": { + "type": "number" + } + }, + "required": [ + "id", + "title", + "count" + ] + } + } + } + } + } + }, + "operationId": "get-epm-categories" + } + }, + "/epm/packages": { + "get": { + "summary": "EPM - Packages - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/search_result" + } + } + } + } + } + }, + "operationId": "get-epm-list" + }, + "parameters": [] + }, + "/epm/packages/{pkgkey}": { + "get": { + "summary": "EPM - Packages - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "allOf": [ + { + "properties": { + "response": { + "$ref": "#/components/schemas/package_info" + } + } + }, + { + "properties": { + "status": { + "type": "string", + "enum": [ + "installed", + "not_installed" + ] + }, + "savedObject": { + "type": "string" + } + }, + "required": [ + "status", + "savedObject" + ] + } + ] + } + } + } + } + }, + "operationId": "get-epm-package-pkgkey", + "security": [ + { + "basicAuth": [] + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "pkgkey", + "in": "path", + "required": true + } + ], + "post": { + "summary": "EPM - Packages - Install", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "response" + ] + } + } + } + } + }, + "operationId": "post-epm-install-pkgkey", + "description": "", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "delete": { + "summary": "EPM - Packages - Delete", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "response" + ] + } + } + } + } + }, + "operationId": "post-epm-delete-pkgkey", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agents/{agentId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Agent - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "type": "object" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-fleet-agents-agentId" + }, + "put": { + "summary": "Fleet - Agent - Update", + "tags": [], + "responses": {}, + "operationId": "put-fleet-agents-agentId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "delete": { + "summary": "Fleet - Agent - Delete", + "tags": [], + "responses": {}, + "operationId": "delete-fleet-agents-agentId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/install/{osType}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "osType", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Get OS install script", + "tags": [], + "responses": {}, + "operationId": "get-fleet-install-osType" + } + }, + "/package_policies": { + "get": { + "summary": "PackagePolicies - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/package_policy" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "items" + ] + } + } + } + } + }, + "operationId": "get-packagePolicies", + "security": [], + "parameters": [] + }, + "parameters": [], + "post": { + "summary": "PackagePolicies - Create", + "operationId": "post-packagePolicies", + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_package_policy" + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/package_policies/{packagePolicyId}": { + "get": { + "summary": "PackagePolicies - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/package_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-packagePolicies-packagePolicyId" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "packagePolicyId", + "in": "path", + "required": true + } + ], + "put": { + "summary": "PackagePolicies - Update", + "operationId": "put-packagePolicies-packagePolicyId", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/package_policy" + }, + "sucess": { + "type": "boolean" + } + }, + "required": [ + "item", + "sucess" + ] + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/setup": { + "post": { + "summary": "Ingest Manager - Setup", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + }, + "operationId": "post-setup", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + } + }, + "components": { + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "Enrollment API Key": { + "name": "Authorization", + "type": "apiKey", + "in": "header", + "description": "e.g. Authorization: ApiKey base64EnrollmentApiKey" + }, + "Access API Key": { + "name": "Authorization", + "type": "apiKey", + "in": "header", + "description": "e.g. Authorization: ApiKey base64AccessApiKey" + } + }, + "parameters": { + "page_size": { + "name": "perPage", + "in": "query", + "description": "The number of items to return", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + }, + "page_index": { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + "kuery": { + "name": "kuery", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + "kbn_xsrf": { + "schema": { + "type": "string" + }, + "in": "header", + "name": "kbn-xsrf", + "required": true + } + }, + "schemas": { + "new_agent_policy": { + "title": "NewAgentPolicy", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "new_package_policy": { + "title": "NewPackagePolicy", + "type": "object", + "description": "", + "properties": { + "enabled": { + "type": "boolean" + }, + "package": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "name", + "version", + "title" + ] + }, + "namespace": { + "type": "string" + }, + "output_id": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "array", + "items": { + "type": "string" + } + }, + "streams": { + "type": "array", + "items": {} + }, + "config": { + "type": "object" + }, + "vars": { + "type": "object" + } + }, + "required": [ + "type", + "enabled", + "streams" + ] + } + }, + "policy_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "output_id", + "inputs", + "policy_id", + "name" + ] + }, + "package_policy": { + "title": "PackagePolicy", + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "revision": { + "type": "number" + }, + "inputs": { + "type": "array", + "items": {} + } + }, + "required": [ + "id", + "revision" + ] + }, + { + "$ref": "#/components/schemas/new_package_policy" + } + ] + }, + "agent_policy": { + "allOf": [ + { + "$ref": "#/components/schemas/new_agent_policy" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "packagePolicies": { + "oneOf": [ + { + "items": { + "type": "string" + } + }, + { + "items": { + "$ref": "#/components/schemas/package_policy" + } + } + ], + "type": "array" + }, + "updated_on": { + "type": "string", + "format": "date-time" + }, + "updated_by": { + "type": "string" + }, + "revision": { + "type": "number" + }, + "agents": { + "type": "number" + } + }, + "required": [ + "id", + "status" + ] + } + ] + }, + "agent_metadata": { + "title": "AgentMetadata", + "type": "object" + }, + "new_agent_event": { + "title": "NewAgentEvent", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "STATE", + "ERROR", + "ACTION_RESULT", + "ACTION" + ] + }, + "subtype": { + "type": "string", + "enum": [ + "RUNNING", + "STARTING", + "IN_PROGRESS", + "CONFIG", + "FAILED", + "STOPPING", + "STOPPED", + "DEGRADED", + "DATA_DUMP", + "ACKNOWLEDGED", + "UNKNOWN" + ] + }, + "timestamp": { + "type": "string" + }, + "message": { + "type": "string" + }, + "payload": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "policy_id": { + "type": "string" + }, + "stream_id": { + "type": "string" + }, + "action_id": { + "type": "string" + } + }, + "required": [ + "type", + "subtype", + "timestamp", + "message", + "agent_id" + ] + }, + "upgrade_agent": { + "title": "UpgradeAgent", + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + } + }, + "required": [ + "version" + ] + } + ] + }, + "bulk_upgrade_agents": { + "title": "BulkUpgradeAgents", + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "agents": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "version", + "agents" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + }, + "agents": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "version", + "agents" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + }, + "agents": { + "type": "string" + } + }, + "required": [ + "version", + "agents" + ] + } + ] + }, + "agent_type": { + "type": "string", + "title": "AgentType", + "enum": [ + "PERMANENT", + "EPHEMERAL", + "TEMPORARY" + ] + }, + "agent_event": { + "title": "AgentEvent", + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + { + "$ref": "#/components/schemas/new_agent_event" + } + ] + }, + "agent_status": { + "type": "string", + "title": "AgentStatus", + "enum": [ + "offline", + "error", + "online", + "inactive", + "warning" + ] + }, + "agent": { + "title": "Agent", + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/agent_type" + }, + "active": { + "type": "boolean" + }, + "enrolled_at": { + "type": "string" + }, + "unenrolled_at": { + "type": "string" + }, + "unenrollment_started_at": { + "type": "string" + }, + "shared_id": { + "type": "string" + }, + "access_api_key_id": { + "type": "string" + }, + "default_api_key_id": { + "type": "string" + }, + "policy_id": { + "type": "string" + }, + "policy_revision": { + "type": "number" + }, + "last_checkin": { + "type": "string" + }, + "user_provided_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "local_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "id": { + "type": "string" + }, + "current_error_events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/agent_event" + } + }, + "access_api_key": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/agent_status" + }, + "default_api_key": { + "type": "string" + } + }, + "required": [ + "type", + "active", + "enrolled_at", + "id", + "current_error_events", + "status" + ] + }, + "search_result": { + "title": "SearchResult", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "download": { + "type": "string" + }, + "icons": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + }, + "status": { + "type": "string" + }, + "savedObject": { + "type": "object" + } + }, + "required": [ + "description", + "download", + "icons", + "name", + "path", + "title", + "type", + "version", + "status" + ] + }, + "package_info": { + "title": "PackageInfo", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "readme": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "requirement": { + "oneOf": [ + { + "properties": { + "kibana": { + "type": "object", + "properties": { + "versions": { + "type": "string" + } + } + } + } + }, + { + "properties": { + "elasticsearch": { + "type": "object", + "properties": { + "versions": { + "type": "string" + } + } + } + } + } + ], + "type": "object" + }, + "screenshots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "size": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "src", + "path" + ] + } + }, + "icons": { + "type": "array", + "items": { + "type": "string" + } + }, + "assets": { + "type": "array", + "items": { + "type": "string" + } + }, + "internal": { + "type": "boolean" + }, + "format_version": { + "type": "string" + }, + "data_streams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "name": { + "type": "string" + }, + "release": { + "type": "string" + }, + "ingeset_pipeline": { + "type": "string" + }, + "vars": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "default": { + "type": "string" + } + }, + "required": [ + "name", + "default" + ] + } + }, + "type": { + "type": "string" + }, + "package": { + "type": "string" + } + }, + "required": [ + "title", + "name", + "release", + "ingeset_pipeline", + "type", + "package" + ] + } + }, + "download": { + "type": "string" + }, + "path": { + "type": "string" + }, + "removable": { + "type": "boolean" + } + }, + "required": [ + "name", + "title", + "version", + "description", + "type", + "categories", + "requirement", + "assets", + "format_version", + "download", + "path" + ] + } + } + }, + "security": [ + { + "basicAuth": [] + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml b/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml new file mode 100644 index 0000000000000..9ab85ab2b8232 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml @@ -0,0 +1,1327 @@ +openapi: 3.0.0 +info: + title: Ingest Manager + version: '0.2' + contact: + name: Ingest Team + license: + name: Elastic +servers: + - url: 'http://localhost:5601/api/fleet' + description: local +paths: + /agent_policies: + get: + summary: Agent policy - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/agent_policy' + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + - total + - page + - perPage + operationId: agent-policy-list + parameters: + - $ref: '#/components/parameters/page_size' + - $ref: '#/components/parameters/page_index' + - $ref: '#/components/parameters/kuery' + description: '' + post: + summary: Agent policy - Create + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + operationId: post-agent-policy + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_agent_policy' + security: [] + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agent_policies/{agentPolicyId}': + parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true + get: + summary: Agent policy - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + operationId: agent-policy-info + description: Get one agent policy + parameters: [] + put: + summary: Agent policy - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + operationId: put-agent-policy-agentPolicyId + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_agent_policy' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agent_policies/{agentPolicyId}/copy': + parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true + post: + summary: Agent policy - copy one policy + operationId: agent-policy-copy + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + required: + - name + description: '' + description: Copies one agent policy + /agent_policies/delete: + post: + summary: Agent policy - Delete + operationId: post-agent-policy-delete + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success + requestBody: + content: + application/json: + schema: + type: object + properties: + agentPolicyIds: + type: array + items: + type: string + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + parameters: [] + /agent-status: + get: + summary: Fleet - Agent - Status for policy + tags: [] + responses: {} + operationId: get-fleet-agent-status + parameters: + - schema: + type: string + name: policyId + in: query + required: false + /agents: + get: + summary: Fleet - Agent - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + list: + type: array + items: + type: object + total: + type: number + page: + type: number + perPage: + type: number + required: + - list + - total + - page + - perPage + operationId: get-fleet-agents + security: + - basicAuth: [] + '/agents/{agentId}/acks': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Acks + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - acks + required: + - action + operationId: post-fleet-agents-agentId-acks + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: {} + '/agents/{agentId}/checkin': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Check In + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - checkin + actions: + type: array + items: + type: object + properties: + agent_id: + type: string + data: + type: object + id: + type: string + created_at: + type: string + format: date-time + type: + type: string + required: + - agent_id + - data + - id + - created_at + - type + operationId: post-fleet-agents-agentId-checkin + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + security: + - Access API Key: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + local_metadata: + $ref: '#/components/schemas/agent_metadata' + events: + type: array + items: + $ref: '#/components/schemas/new_agent_event' + '/agents/{agentId}/events': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + get: + summary: Fleet - Agent - Events + tags: [] + responses: {} + operationId: get-fleet-agents-agentId-events + '/agents/{agentId}/unenroll': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Unenroll + tags: [] + responses: {} + operationId: post-fleet-agents-unenroll + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean + '/agents/{agentId}/upgrade': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + operationId: post-fleet-agents-upgrade + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + /agents/bulk_upgrade: + post: + summary: Fleet - Agent - Bulk Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_upgrade_agents' + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + operationId: post-fleet-agents-bulk-upgrade + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_upgrade_agents' + /agents/enroll: + post: + summary: Fleet - Agent - Enroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + item: + $ref: '#/components/schemas/agent' + operationId: post-fleet-agents-enroll + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + shared_id: + type: string + metadata: + type: object + required: + - local + - user_provided + properties: + local: + $ref: '#/components/schemas/agent_metadata' + user_provided: + $ref: '#/components/schemas/agent_metadata' + required: + - type + - metadata + security: + - Enrollment API Key: [] + /agents/setup: + get: + summary: Agents setup - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + operationId: get-agents-setup + security: + - basicAuth: [] + post: + summary: Agents setup - Create + operationId: post-agents-setup + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + requestBody: + content: + application/json: + schema: + type: object + properties: + admin_username: + type: string + admin_password: + type: string + required: + - admin_username + - admin_password + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /enrollment-api-keys: + get: + summary: Enrollment - List + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys + parameters: [] + post: + summary: Enrollment - Create + tags: [] + responses: {} + operationId: post-fleet-enrollment-api-keys + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/enrollment-api-keys/{keyId}': + parameters: + - schema: + type: string + name: keyId + in: path + required: true + get: + summary: Enrollment - Info + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys-keyId + delete: + summary: Enrollment - Delete + tags: [] + responses: {} + operationId: delete-fleet-enrollment-api-keys-keyId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /epm/categories: + get: + summary: EPM - Categories + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + count: + type: number + required: + - id + - title + - count + operationId: get-epm-categories + /epm/packages: + get: + summary: EPM - Packages - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/search_result' + operationId: get-epm-list + parameters: [] + '/epm/packages/{pkgkey}': + get: + summary: EPM - Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + response: + $ref: '#/components/schemas/package_info' + - properties: + status: + type: string + enum: + - installed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-epm-package-pkgkey + security: + - basicAuth: [] + parameters: + - schema: + type: string + name: pkgkey + in: path + required: true + post: + summary: EPM - Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-install-pkgkey + description: '' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + delete: + summary: EPM - Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-delete-pkgkey + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agents/{agentId}': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + get: + summary: Fleet - Agent - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + type: object + required: + - item + operationId: get-fleet-agents-agentId + put: + summary: Fleet - Agent - Update + tags: [] + responses: {} + operationId: put-fleet-agents-agentId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + delete: + summary: Fleet - Agent - Delete + tags: [] + responses: {} + operationId: delete-fleet-agents-agentId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/install/{osType}': + parameters: + - schema: + type: string + name: osType + in: path + required: true + get: + summary: Fleet - Get OS install script + tags: [] + responses: {} + operationId: get-fleet-install-osType + /package_policies: + get: + summary: PackagePolicies - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/package_policy' + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + operationId: get-packagePolicies + security: [] + parameters: [] + parameters: [] + post: + summary: PackagePolicies - Create + operationId: post-packagePolicies + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_package_policy' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/package_policies/{packagePolicyId}': + get: + summary: PackagePolicies - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/package_policy' + required: + - item + operationId: get-packagePolicies-packagePolicyId + parameters: + - schema: + type: string + name: packagePolicyId + in: path + required: true + put: + summary: PackagePolicies - Update + operationId: put-packagePolicies-packagePolicyId + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/package_policy' + sucess: + type: boolean + required: + - item + - sucess + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /setup: + post: + summary: Ingest Manager - Setup + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + operationId: post-setup + parameters: + - $ref: '#/components/parameters/kbn_xsrf' +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + Enrollment API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64EnrollmentApiKey' + Access API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64AccessApiKey' + parameters: + page_size: + name: perPage + in: query + description: The number of items to return + required: false + schema: + type: integer + default: 50 + page_index: + name: page + in: query + required: false + schema: + type: integer + default: 1 + kuery: + name: kuery + in: query + required: false + schema: + type: string + kbn_xsrf: + schema: + type: string + in: header + name: kbn-xsrf + required: true + schemas: + new_agent_policy: + title: NewAgentPolicy + type: object + properties: + name: + type: string + namespace: + type: string + description: + type: string + new_package_policy: + title: NewPackagePolicy + type: object + description: '' + properties: + enabled: + type: boolean + package: + type: object + properties: + name: + type: string + version: + type: string + title: + type: string + required: + - name + - version + - title + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string + required: + - output_id + - inputs + - policy_id + - name + package_policy: + title: PackagePolicy + allOf: + - type: object + properties: + id: + type: string + revision: + type: number + inputs: + type: array + items: {} + required: + - id + - revision + - $ref: '#/components/schemas/new_package_policy' + agent_policy: + allOf: + - $ref: '#/components/schemas/new_agent_policy' + - type: object + properties: + id: + type: string + status: + type: string + enum: + - active + - inactive + packagePolicies: + oneOf: + - items: + type: string + - items: + $ref: '#/components/schemas/package_policy' + type: array + updated_on: + type: string + format: date-time + updated_by: + type: string + revision: + type: number + agents: + type: number + required: + - id + - status + agent_metadata: + title: AgentMetadata + type: object + new_agent_event: + title: NewAgentEvent + type: object + properties: + type: + type: string + enum: + - STATE + - ERROR + - ACTION_RESULT + - ACTION + subtype: + type: string + enum: + - RUNNING + - STARTING + - IN_PROGRESS + - CONFIG + - FAILED + - STOPPING + - STOPPED + - DEGRADED + - DATA_DUMP + - ACKNOWLEDGED + - UNKNOWN + timestamp: + type: string + message: + type: string + payload: + type: string + agent_id: + type: string + policy_id: + type: string + stream_id: + type: string + action_id: + type: string + required: + - type + - subtype + - timestamp + - message + - agent_id + upgrade_agent: + title: UpgradeAgent + oneOf: + - type: object + properties: + version: + type: string + required: + - version + - type: object + properties: + version: + type: string + source_uri: + type: string + required: + - version + bulk_upgrade_agents: + title: BulkUpgradeAgents + oneOf: + - type: object + properties: + version: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: string + required: + - version + - agents + agent_type: + type: string + title: AgentType + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + agent_event: + title: AgentEvent + allOf: + - type: object + properties: + id: + type: string + required: + - id + - $ref: '#/components/schemas/new_agent_event' + agent_status: + type: string + title: AgentStatus + enum: + - offline + - error + - online + - inactive + - warning + agent: + title: Agent + type: object + properties: + type: + $ref: '#/components/schemas/agent_type' + active: + type: boolean + enrolled_at: + type: string + unenrolled_at: + type: string + unenrollment_started_at: + type: string + shared_id: + type: string + access_api_key_id: + type: string + default_api_key_id: + type: string + policy_id: + type: string + policy_revision: + type: number + last_checkin: + type: string + user_provided_metadata: + $ref: '#/components/schemas/agent_metadata' + local_metadata: + $ref: '#/components/schemas/agent_metadata' + id: + type: string + current_error_events: + type: array + items: + $ref: '#/components/schemas/agent_event' + access_api_key: + type: string + status: + $ref: '#/components/schemas/agent_status' + default_api_key: + type: string + required: + - type + - active + - enrolled_at + - id + - current_error_events + - status + search_result: + title: SearchResult + type: object + properties: + description: + type: string + download: + type: string + icons: + type: string + name: + type: string + path: + type: string + title: + type: string + type: + type: string + version: + type: string + status: + type: string + savedObject: + type: object + required: + - description + - download + - icons + - name + - path + - title + - type + - version + - status + package_info: + title: PackageInfo + type: object + properties: + name: + type: string + title: + type: string + version: + type: string + readme: + type: string + description: + type: string + type: + type: string + categories: + type: array + items: + type: string + requirement: + oneOf: + - properties: + kibana: + type: object + properties: + versions: + type: string + - properties: + elasticsearch: + type: object + properties: + versions: + type: string + type: object + screenshots: + type: array + items: + type: object + properties: + src: + type: string + path: + type: string + title: + type: string + size: + type: string + type: + type: string + required: + - src + - path + icons: + type: array + items: + type: string + assets: + type: array + items: + type: string + internal: + type: boolean + format_version: + type: string + data_streams: + type: array + items: + type: object + properties: + title: + type: string + name: + type: string + release: + type: string + ingeset_pipeline: + type: string + vars: + type: array + items: + type: object + properties: + name: + type: string + default: + type: string + required: + - name + - default + type: + type: string + package: + type: string + required: + - title + - name + - release + - ingeset_pipeline + - type + - package + download: + type: string + path: + type: string + removable: + type: boolean + required: + - name + - title + - version + - description + - type + - categories + - requirement + - assets + - format_version + - download + - path +security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/README.md b/x-pack/plugins/ingest_manager/common/openapi/components/README.md new file mode 100644 index 0000000000000..1579c2d2b6eb5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/README.md @@ -0,0 +1,13 @@ +Reusable components +=========== + +* Created the following folders for the various OpenAPI component types: + - `schemas` - reusable [Schema Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) + - `responses` - reusable [Response Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) + - `parameters` - reusable [Parameter Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) + - `examples` - reusable [Example Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject) + - `headers` - reusable [Header Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) + - `request_bodies` - reusable [Request Body Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject) + - `links` - reusable [Link Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject) + - `callbacks` - reusable [Callback Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject) + - `security_schemes` - reusable [Security Scheme Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject) diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml new file mode 100644 index 0000000000000..3d8dfae634e68 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml @@ -0,0 +1,5 @@ +schema: + type: string +in: header +name: kbn-xsrf +required: true diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml new file mode 100644 index 0000000000000..b96ffd54d37ce --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml @@ -0,0 +1,5 @@ +name: kuery +in: query +required: false +schema: + type: string diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml new file mode 100644 index 0000000000000..908c19583045b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml @@ -0,0 +1,6 @@ +name: page +in: query +required: false +schema: + type: integer + default: 1 diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml new file mode 100644 index 0000000000000..698491def3b39 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml @@ -0,0 +1,7 @@ +name: perPage +in: query +description: The number of items to return +required: false +schema: + type: integer + default: 50 diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml new file mode 100644 index 0000000000000..31e2072ddefbe --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml @@ -0,0 +1,3 @@ +type: string +title: AccessApiKey +format: byte diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml new file mode 100644 index 0000000000000..df106093a8d8d --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml @@ -0,0 +1,48 @@ +title: Agent +type: object +properties: + type: + $ref: ./agent_type.yaml + active: + type: boolean + enrolled_at: + type: string + unenrolled_at: + type: string + unenrollment_started_at: + type: string + shared_id: + type: string + access_api_key_id: + type: string + default_api_key_id: + type: string + policy_id: + type: string + policy_revision: + type: number + last_checkin: + type: string + user_provided_metadata: + $ref: ./agent_metadata.yaml + local_metadata: + $ref: ./agent_metadata.yaml + id: + type: string + current_error_events: + type: array + items: + $ref: ./agent_event.yaml + access_api_key: + type: string + status: + $ref: ./agent_status.yaml + default_api_key: + type: string +required: + - type + - active + - enrolled_at + - id + - current_error_events + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml new file mode 100644 index 0000000000000..ada709378a9b1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml @@ -0,0 +1,9 @@ +title: AgentEvent +allOf: + - type: object + properties: + id: + type: string + required: + - id + - $ref: ./new_agent_event.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml new file mode 100644 index 0000000000000..d37321f59a58b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml @@ -0,0 +1,2 @@ +title: AgentMetadata +type: object diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml new file mode 100644 index 0000000000000..7395e45365ea9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml @@ -0,0 +1,30 @@ +allOf: + - $ref: ./new_agent_policy.yaml + - type: object + properties: + id: + type: string + status: + type: string + enum: + - active + - inactive + packagePolicies: + oneOf: + - items: + type: string + - items: + $ref: ./package_policy.yaml + type: array + updated_on: + type: string + format: date-time + updated_by: + type: string + revision: + type: number + agents: + type: number + required: + - id + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml new file mode 100644 index 0000000000000..076a7cc5036bb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml @@ -0,0 +1,8 @@ +type: string +title: AgentStatus +enum: + - offline + - error + - online + - inactive + - warning diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml new file mode 100644 index 0000000000000..da42f95c9e1d9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml @@ -0,0 +1,6 @@ +type: string +title: AgentType +enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml new file mode 100644 index 0000000000000..da06aa6fa8252 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml @@ -0,0 +1,37 @@ +title: BulkUpgradeAgents +oneOf: + - type: object + properties: + version: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: string + required: + - version + - agents diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml new file mode 100644 index 0000000000000..3efe77b3bd606 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml @@ -0,0 +1,3 @@ +type: string +title: EnrollmentApiKey +format: byte diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml new file mode 100644 index 0000000000000..ee4ddfb5f004d --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml @@ -0,0 +1,44 @@ +title: NewAgentEvent +type: object +properties: + type: + type: string + enum: + - STATE + - ERROR + - ACTION_RESULT + - ACTION + subtype: + type: string + enum: + - RUNNING + - STARTING + - IN_PROGRESS + - CONFIG + - FAILED + - STOPPING + - STOPPED + - DEGRADED + - DATA_DUMP + - ACKNOWLEDGED + - UNKNOWN + timestamp: + type: string + message: + type: string + payload: + type: string + agent_id: + type: string + policy_id: + type: string + stream_id: + type: string + action_id: + type: string +required: + - type + - subtype + - timestamp + - message + - agent_id diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml new file mode 100644 index 0000000000000..7070876cbea59 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml @@ -0,0 +1,9 @@ +title: NewAgentPolicy +type: object +properties: + name: + type: string + namespace: + type: string + description: + type: string diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml new file mode 100644 index 0000000000000..61b1fa678d407 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml @@ -0,0 +1,58 @@ +title: NewPackagePolicy +type: object +description: '' +properties: + enabled: + type: boolean + package: + type: object + properties: + name: + type: string + version: + type: string + title: + type: string + required: + - name + - version + - title + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string +required: + - output_id + - inputs + - policy_id + - name diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml new file mode 100644 index 0000000000000..3e0742c1879cb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml @@ -0,0 +1,118 @@ +title: PackageInfo +type: object +properties: + name: + type: string + title: + type: string + version: + type: string + readme: + type: string + description: + type: string + type: + type: string + categories: + type: array + items: + type: string + requirement: + oneOf: + - properties: + kibana: + type: object + properties: + versions: + type: string + - properties: + elasticsearch: + type: object + properties: + versions: + type: string + type: object + screenshots: + type: array + items: + type: object + properties: + src: + type: string + path: + type: string + title: + type: string + size: + type: string + type: + type: string + required: + - src + - path + icons: + type: array + items: + type: string + assets: + type: array + items: + type: string + internal: + type: boolean + format_version: + type: string + data_streams: + type: array + items: + type: object + properties: + title: + type: string + name: + type: string + release: + type: string + ingeset_pipeline: + type: string + vars: + type: array + items: + type: object + properties: + name: + type: string + default: + type: string + required: + - name + - default + type: + type: string + package: + type: string + required: + - title + - name + - release + - ingeset_pipeline + - type + - package + download: + type: string + path: + type: string + removable: + type: boolean +required: + - name + - title + - version + - description + - type + - categories + - requirement + - assets + - format_version + - download + - path diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml new file mode 100644 index 0000000000000..99bc64f793379 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml @@ -0,0 +1,15 @@ +title: PackagePolicy +allOf: + - type: object + properties: + id: + type: string + revision: + type: number + inputs: + type: array + items: {} + required: + - id + - revision + - $ref: ./new_package_policy.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml new file mode 100644 index 0000000000000..b67ff61c5ab60 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml @@ -0,0 +1,33 @@ +title: SearchResult +type: object +properties: + description: + type: string + download: + type: string + icons: + type: string + name: + type: string + path: + type: string + title: + type: string + type: + type: string + version: + type: string + status: + type: string + savedObject: + type: object +required: + - description + - download + - icons + - name + - path + - title + - type + - version + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml new file mode 100644 index 0000000000000..11a2b5846ba1e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml @@ -0,0 +1,16 @@ +title: UpgradeAgent +oneOf: + - type: object + properties: + version: + type: string + required: + - version + - type: object + properties: + version: + type: string + source_uri: + type: string + required: + - version diff --git a/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml b/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml new file mode 100644 index 0000000000000..791d3da56783e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml @@ -0,0 +1,77 @@ +openapi: 3.0.0 +info: + title: Ingest Manager + version: '0.2' + contact: + name: Ingest Team + license: + name: Elastic +servers: + - url: 'http://localhost:5601/api/fleet' + description: local +paths: + /agent_policies: + $ref: paths/agent_policies.yaml + '/agent_policies/{agentPolicyId}': + $ref: 'paths/agent_policies@{agent_policy_id}.yaml' + '/agent_policies/{agentPolicyId}/copy': + $ref: 'paths/agent_policies@{agent_policy_id}@copy.yaml' + /agent_policies/delete: + $ref: paths/agent_policies@delete.yaml + /agent-status: + $ref: paths/agent_status.yaml + /agents: + $ref: paths/agents.yaml + '/agents/{agentId}/acks': + $ref: 'paths/agents@{agent_id}@acks.yaml' + '/agents/{agentId}/checkin': + $ref: 'paths/agents@{agent_id}@checkin.yaml' + '/agents/{agentId}/events': + $ref: 'paths/agents@{agent_id}@events.yaml' + '/agents/{agentId}/unenroll': + $ref: 'paths/agents@{agent_id}@unenroll.yaml' + '/agents/{agentId}/upgrade': + $ref: 'paths/agents@{agent_id}@upgrade.yaml' + /agents/bulk_upgrade: + $ref: paths/agents@bulk_upgrade.yaml + /agents/enroll: + $ref: paths/agents@enroll.yaml + /agents/setup: + $ref: paths/agents@setup.yaml + /enrollment-api-keys: + $ref: paths/enrollment_api_keys.yaml + '/enrollment-api-keys/{keyId}': + $ref: 'paths/enrollment_api_keys@{key_id}.yaml' + /epm/categories: + $ref: paths/epm@categories.yaml + /epm/packages: + $ref: paths/epm@packages.yaml + '/epm/packages/{pkgkey}': + $ref: 'paths/epm@packages@{pkgkey}.yaml' + '/agents/{agentId}': + $ref: 'paths/agents@{agent_id}.yaml' + '/install/{osType}': + $ref: 'paths/install@{os_type}.yaml' + /package_policies: + $ref: paths/package_policies.yaml + '/package_policies/{packagePolicyId}': + $ref: 'paths/package_policies@{package_policy_id}.yaml' + /setup: + $ref: paths/setup.yaml +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + Enrollment API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64EnrollmentApiKey' + Access API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64AccessApiKey' +security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/README.md b/x-pack/plugins/ingest_manager/common/openapi/paths/README.md new file mode 100644 index 0000000000000..f5003e3e3473b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/README.md @@ -0,0 +1,130 @@ +Paths +===== + +Organize our path definitions within this folder. We will reference our paths from our main `openapi.json` entrypoint file. + +It may help us to adopt some conventions: + +* path separator token (e.g. `@`) or subfolders +* path parameter (e.g. `{example}`) +* file-per-path or file-per-operation + +There are different benefits and drawbacks to each decision. + +We can adopt any organization we wish. We have some tips for organizing paths based on common practices. + +## Each path in a separate file + +Use a predefined "path separator" and keep all of our path files in the top level of the `paths` folder. + +``` +paths/ +├── README.md +├── agent_policies.yaml +├── agent_policies@delete.yaml +├── agent_policies@{agent_policy_id}.yaml +├── agent_policies@{agent_policy_id}@copy.yaml +├── agent_status.yaml +├── agents.yaml +├── agents@bulk_upgrade.yaml +├── agents@enroll.yaml +├── agents@setup.yaml +├── agents@{agent_id}.yaml +├── agents@{agent_id}@acks.yaml +├── agents@{agent_id}@checkin.yaml +├── agents@{agent_id}@events.yaml +├── agents@{agent_id}@unenroll.yaml +├── agents@{agent_id}@upgrade.yaml +├── enrollment_api_keys.yaml +├── enrollment_api_keys@{key_id}.yaml +├── epm@categories.yaml +├── epm@packages.yaml +├── epm@packages@{pkgkey}.yaml +├── install@{os_type}.yaml +├── package_policies.yaml +├── package_policies@{package_policy_id}.yaml +└── setup.yaml +``` + +Redocly recommends using the `@` character for this case. + +In addition, Redocly recommends placing path parameters within `{}` curly braces if we adopt this style. + +#### Motivations + +* Quickly see a list of all paths. Many people think in terms of the "number" of "endpoints" (paths), and not the "number" of "operations" (paths * http methods). + +* Only the "file-per-path" option is semantically correct with the OpenAPI Specification 3.0.2. However, Redocly's openapi-cli will build valid bundles for any of the other options too. + + +#### Drawbacks + +* This may require multiple definitions per http method within a single file. +* It requires settling on a path separator (that is allowed to be used in filenames) and sticking to that convention. + +## Each operation in a separate file + +We may also place each operation in a separate file. + +In this case, if we want all paths at the top-level, we can concatenate the http method to the path name. Similar to the above option, we can + +### Files at top-level of `paths` + +We may name our files with some concatenation for the http method. For example, following a convention such as: `-.json`. + +#### Motivations + +* Quickly see all operations without needing to navigate subfolders. + +#### Drawbacks + +* Adopting an unusual path separator convention, instead of using subfolders. + +### Use subfolders to mirror API path structure + +Example: +``` +GET /customers + +/paths/customers/get.json +``` + +In this case, the path id defined within subfolders which mirror the API URL structure. + +Example with path parameter: +``` +GET /customers/{id} + +/paths/customers/{id}/get.json +``` + +#### Motivations + +It matches the URL structure. + +It is pretty easy to reference: + +```json +paths: + '/customers/{id}': + get: + $ref: ./paths/customers/{id}/get.json + put: + $ref: ./paths/customers/{id}/put.json +``` + +#### Drawbacks + +If we have a lot of nested folders, it may be confusing to reference our schemas. + +Example +``` +file: /paths/customers/{id}/timeline/{messageId}/get.json + +# excerpt of file + headers: + Rate-Limit-Remaining: + $ref: ../../../../../components/headers/Rate-Limit-Remaining.json + +``` +Notice the `../../../../../` in the ref which requires some attention to formulate correctly. While openapi-cli has a linter which suggests possible refs when there is a mistake, this is still a net drawback for APIs with deep paths. diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml new file mode 100644 index 0000000000000..2ba14fba7232b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml @@ -0,0 +1,54 @@ +get: + summary: Agent policy - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/agent_policy.yaml + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + - total + - page + - perPage + operationId: agent-policy-list + parameters: + - $ref: ../components/parameters/page_size.yaml + - $ref: ../components/parameters/page_index.yaml + - $ref: ../components/parameters/kuery.yaml + description: '' +post: + summary: Agent policy - Create + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + operationId: post-agent-policy + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_agent_policy.yaml + security: [] + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml new file mode 100644 index 0000000000000..ae975274d80e5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml @@ -0,0 +1,33 @@ +post: + summary: Agent policy - Delete + operationId: post-agent-policy-delete + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success + requestBody: + content: + application/json: + schema: + type: object + properties: + agentPolicyIds: + type: array + items: + type: string + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +parameters: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml new file mode 100644 index 0000000000000..15910b0116b7f --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml @@ -0,0 +1,47 @@ +parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true +get: + summary: Agent policy - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + operationId: agent-policy-info + description: Get one agent policy + parameters: [] +put: + summary: Agent policy - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + operationId: put-agent-policy-agentPolicyId + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_agent_policy.yaml + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml new file mode 100644 index 0000000000000..4b42f8cab0677 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml @@ -0,0 +1,35 @@ +parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true +post: + summary: Agent policy - copy one policy + operationId: agent-policy-copy + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + required: + - name + description: '' + description: Copies one agent policy diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml new file mode 100644 index 0000000000000..77ec9e85069a2 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml @@ -0,0 +1,11 @@ +get: + summary: Fleet - Agent - Status for policy + tags: [] + responses: {} + operationId: get-fleet-agent-status + parameters: + - schema: + type: string + name: policyId + in: query + required: false diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml new file mode 100644 index 0000000000000..e5039bc2caccf --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml @@ -0,0 +1,29 @@ +get: + summary: Fleet - Agent - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + list: + type: array + items: + type: object + total: + type: number + page: + type: number + perPage: + type: number + required: + - list + - total + - page + - perPage + operationId: get-fleet-agents + security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml new file mode 100644 index 0000000000000..2092fbf000ab8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml @@ -0,0 +1,25 @@ +post: + summary: Fleet - Agent - Bulk Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/bulk_upgrade_agents.yaml + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + operationId: post-fleet-agents-bulk-upgrade + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/bulk_upgrade_agents.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml new file mode 100644 index 0000000000000..a0c1c8c28e721 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml @@ -0,0 +1,47 @@ +post: + summary: Fleet - Agent - Enroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + item: + $ref: ../components/schemas/agent.yaml + operationId: post-fleet-agents-enroll + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + shared_id: + type: string + metadata: + type: object + required: + - local + - user_provided + properties: + local: + $ref: ../components/schemas/agent_metadata.yaml + user_provided: + $ref: ../components/schemas/agent_metadata.yaml + required: + - type + - metadata + security: + - Enrollment API Key: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml new file mode 100644 index 0000000000000..87556dca0afbb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml @@ -0,0 +1,48 @@ +get: + summary: Agents setup - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + operationId: get-agents-setup + security: + - basicAuth: [] +post: + summary: Agents setup - Create + operationId: post-agents-setup + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + requestBody: + content: + application/json: + schema: + type: object + properties: + admin_username: + type: string + admin_password: + type: string + required: + - admin_username + - admin_password + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml new file mode 100644 index 0000000000000..e65c80d8fae88 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml @@ -0,0 +1,36 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +get: + summary: Fleet - Agent - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + type: object + required: + - item + operationId: get-fleet-agents-agentId +put: + summary: Fleet - Agent - Update + tags: [] + responses: {} + operationId: put-fleet-agents-agentId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +delete: + summary: Fleet - Agent - Delete + tags: [] + responses: {} + operationId: delete-fleet-agents-agentId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml new file mode 100644 index 0000000000000..6728554bf542e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml @@ -0,0 +1,32 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Acks + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - acks + required: + - action + operationId: post-fleet-agents-agentId-acks + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: {} diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml new file mode 100644 index 0000000000000..cc797c7356603 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml @@ -0,0 +1,60 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Check In + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - checkin + actions: + type: array + items: + type: object + properties: + agent_id: + type: string + data: + type: object + id: + type: string + created_at: + type: string + format: date-time + type: + type: string + required: + - agent_id + - data + - id + - created_at + - type + operationId: post-fleet-agents-agentId-checkin + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + security: + - Access API Key: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + local_metadata: + $ref: ../components/schemas/agent_metadata.yaml + events: + type: array + items: + $ref: ../components/schemas/new_agent_event.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml new file mode 100644 index 0000000000000..db8d28f72b5a2 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml @@ -0,0 +1,11 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +get: + summary: Fleet - Agent - Events + tags: [] + responses: {} + operationId: get-fleet-agents-agentId-events diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml new file mode 100644 index 0000000000000..00c9cdfbcf4ae --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml @@ -0,0 +1,21 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Unenroll + tags: [] + responses: {} + operationId: post-fleet-agents-unenroll + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml new file mode 100644 index 0000000000000..ce871cac0d068 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml @@ -0,0 +1,32 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + operationId: post-fleet-agents-upgrade + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml new file mode 100644 index 0000000000000..22d27c0596d68 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml @@ -0,0 +1,13 @@ +get: + summary: Enrollment - List + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys + parameters: [] +post: + summary: Enrollment - Create + tags: [] + responses: {} + operationId: post-fleet-enrollment-api-keys + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml new file mode 100644 index 0000000000000..3b43950427e82 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml @@ -0,0 +1,18 @@ +parameters: + - schema: + type: string + name: keyId + in: path + required: true +get: + summary: Enrollment - Info + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys-keyId +delete: + summary: Enrollment - Delete + tags: [] + responses: {} + operationId: delete-fleet-enrollment-api-keys-keyId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml new file mode 100644 index 0000000000000..0fc26a4e5c826 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml @@ -0,0 +1,24 @@ +get: + summary: EPM - Categories + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + count: + type: number + required: + - id + - title + - count + operationId: get-epm-categories diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml new file mode 100644 index 0000000000000..afbe8ee2dc321 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml @@ -0,0 +1,14 @@ +get: + summary: EPM - Packages - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: ../components/schemas/search_result.yaml + operationId: get-epm-list +parameters: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml new file mode 100644 index 0000000000000..43937aa153f50 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml @@ -0,0 +1,91 @@ +get: + summary: EPM - Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + response: + $ref: ../components/schemas/package_info.yaml + - properties: + status: + type: string + enum: + - installed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-epm-package-pkgkey + security: + - basicAuth: [] +parameters: + - schema: + type: string + name: pkgkey + in: path + required: true +post: + summary: EPM - Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-install-pkgkey + description: '' + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +delete: + summary: EPM - Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-delete-pkgkey + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml new file mode 100644 index 0000000000000..80351aa7ae119 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml @@ -0,0 +1,11 @@ +parameters: + - schema: + type: string + name: osType + in: path + required: true +get: + summary: Fleet - Get OS install script + tags: [] + responses: {} + operationId: get-fleet-install-osType diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml new file mode 100644 index 0000000000000..47eca50f0524b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml @@ -0,0 +1,40 @@ +get: + summary: PackagePolicies - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/package_policy.yaml + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + operationId: get-packagePolicies + security: [] + parameters: [] +parameters: [] +post: + summary: PackagePolicies - Create + operationId: post-packagePolicies + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_package_policy.yaml + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml new file mode 100644 index 0000000000000..3b177be3d032e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml @@ -0,0 +1,42 @@ +get: + summary: PackagePolicies - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/package_policy.yaml + required: + - item + operationId: get-packagePolicies-packagePolicyId +parameters: + - schema: + type: string + name: packagePolicyId + in: path + required: true +put: + summary: PackagePolicies - Update + operationId: put-packagePolicies-packagePolicyId + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/package_policy.yaml + sucess: + type: boolean + required: + - item + - sucess + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml new file mode 100644 index 0000000000000..62ad2cb66dacb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml @@ -0,0 +1,25 @@ +post: + summary: Ingest Manager - Setup + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + operationId: post-setup + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json deleted file mode 100644 index 69974a87434a1..0000000000000 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ /dev/null @@ -1,4538 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Ingest Manager", - "version": "0.2", - "contact": { - "name": "Ingest Team" - }, - "license": { - "name": "Elastic" - } - }, - "servers": [ - { - "url": "http://localhost:5601/api/fleet", - "description": "local" - } - ], - "paths": { - "/agent_policies": { - "get": { - "summary": "Agent policy - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["items", "total", "page", "perPage"] - }, - "examples": { - "success": { - "value": { - "items": [ - { - "id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "name": "Default policy", - "namespace": "default", - "description": "Default agent policy created by Kibana", - "status": "active", - "packagePolicies": ["8a5679b0-8fbf-11ea-b2ce-01c4a6127154"], - "is_default": true, - "monitoring_enabled": ["logs", "metrics"], - "revision": 2, - "updated_on": "2020-05-06T17:32:21.905Z", - "updated_by": "system", - "agents": 0 - } - ], - "total": 1, - "page": 1, - "perPage": 50 - } - } - } - } - } - } - }, - "operationId": "agent-policy-list", - "parameters": [ - { - "$ref": "#/components/parameters/pageSizeParam" - }, - { - "$ref": "#/components/parameters/pageIndexParam" - }, - { - "$ref": "#/components/parameters/kueryParam" - } - ], - "description": "" - }, - "post": { - "summary": "Agent policy - Create", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - } - } - } - } - } - }, - "operationId": "post-agent-policy", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAgentPolicy" - } - } - } - }, - "security": [], - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agent_policies/{agentPolicyId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentPolicyId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Agent policy - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - }, - "examples": { - "success": { - "value": { - "item": { - "id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "name": "Default policy", - "namespace": "default", - "description": "Default agent policy created by Kibana", - "status": "active", - "packagePolicies": [ - { - "id": "8a5679b0-8fbf-11ea-b2ce-01c4a6127154", - "name": "system-1", - "namespace": "default", - "package": { - "name": "system", - "title": "System", - "version": "0.0.3" - }, - "enabled": true, - "policy_id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "output_id": "08adc51c-69f3-4294-80e2-24527c6ff73d", - "inputs": [ - { - "type": "logs", - "enabled": true, - "streams": [ - { - "id": "logs-system.auth", - "enabled": true, - "dataset": "system.auth", - "vars": { - "paths": { - "value": ["/var/log/auth.log*", "/var/log/secure*"], - "type": "text" - } - }, - "agent_stream": { - "paths": ["/var/log/auth.log*", "/var/log/secure*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - }, - { - "id": "logs-system.syslog", - "enabled": true, - "dataset": "system.syslog", - "vars": { - "paths": { - "value": ["/var/log/messages*", "/var/log/syslog*"], - "type": "text" - } - }, - "agent_stream": { - "paths": ["/var/log/messages*", "/var/log/syslog*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - } - ] - }, - { - "type": "system/metrics", - "enabled": true, - "streams": [ - { - "id": "system/metrics-system.core", - "enabled": true, - "dataset": "system.core", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["core"], - "core.metrics": "percentages" - } - }, - { - "id": "system/metrics-system.cpu", - "enabled": true, - "dataset": "system.cpu", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["cpu"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.diskio", - "enabled": true, - "dataset": "system.diskio", - "agent_stream": { - "metricsets": ["diskio"] - } - }, - { - "id": "system/metrics-system.entropy", - "enabled": true, - "dataset": "system.entropy", - "agent_stream": { - "metricsets": ["entropy"] - } - }, - { - "id": "system/metrics-system.filesystem", - "enabled": true, - "dataset": "system.filesystem", - "vars": { - "period": { - "value": "1m", - "type": "text" - }, - "processors": { - "value": "- drop_event.when.regexp:\n system.filesystem.mount_point: ^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)\n", - "type": "yaml" - } - }, - "agent_stream": { - "metricsets": ["filesystem"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - } - }, - { - "id": "system/metrics-system.fsstat", - "enabled": true, - "dataset": "system.fsstat", - "vars": { - "period": { - "value": "1m", - "type": "text" - }, - "processors": { - "value": "- drop_event.when.regexp:\n system.filesystem.mount_point: ^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)\n", - "type": "yaml" - } - }, - "agent_stream": { - "metricsets": ["fsstat"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - } - }, - { - "id": "system/metrics-system.load", - "enabled": true, - "dataset": "system.load", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["load"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.memory", - "enabled": true, - "dataset": "system.memory", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["memory"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.network", - "enabled": true, - "dataset": "system.network", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["network"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.network_summary", - "enabled": true, - "dataset": "system.network_summary", - "agent_stream": { - "metricsets": ["network_summary"] - } - }, - { - "id": "system/metrics-system.process", - "enabled": true, - "dataset": "system.process", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["process"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.process_summary", - "enabled": true, - "dataset": "system.process_summary", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["process_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.raid", - "enabled": true, - "dataset": "system.raid", - "agent_stream": { - "metricsets": ["raid"] - } - }, - { - "id": "system/metrics-system.service", - "enabled": true, - "dataset": "system.service", - "agent_stream": { - "metricsets": ["service"] - } - }, - { - "id": "system/metrics-system.socket", - "enabled": true, - "dataset": "system.socket", - "agent_stream": { - "metricsets": ["socket"] - } - }, - { - "id": "system/metrics-system.socket_summary", - "enabled": true, - "dataset": "system.socket_summary", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["socket_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.uptime", - "enabled": true, - "dataset": "system.uptime", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["uptime"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "processes": ".*" - } - }, - { - "id": "system/metrics-system.users", - "enabled": true, - "dataset": "system.users", - "agent_stream": { - "metricsets": ["users"] - } - } - ] - } - ], - "revision": 1 - } - ], - "is_default": true, - "monitoring_enabled": ["logs", "metrics"], - "revision": 2, - "updated_on": "2020-05-06T17:32:21.905Z", - "updated_by": "system" - } - } - } - } - } - } - } - }, - "operationId": "agent-policy-info", - "description": "Get one agent policy", - "parameters": [] - }, - "put": { - "summary": "Agent policy - Update", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - }, - "examples": { - "example-1": { - "value": { - "item": { - "id": "0b7130d0-5a37-11ea-ac2c-25e9ab4ecb2a", - "name": "UPDATED name", - "description": "UPDATED description", - "namespace": "UPDATED namespace", - "updated_on": "Fri Feb 28 2020 16:22:31 GMT-0500 (Eastern Standard Time)", - "updated_by": "elastic", - "packagePolicies": [] - } - } - } - } - } - } - } - }, - "operationId": "put-agent-policy-agentPolicyId", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAgentPolicy" - }, - "examples": { - "example-1": { - "value": { - "name": "UPDATED name", - "description": "UPDATED description", - "namespace": "UPDATED namespace" - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agent_policies/{agentPolicyId}/copy": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentPolicyId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Agent policy - copy one policy", - "operationId": "agent-policy-copy", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - }, - "examples": {} - } - }, - "description": "" - }, - "description": "Copies one agent policy" - } - }, - "/agent_policies/delete": { - "post": { - "summary": "Agent policy - Delete", - "operationId": "post-agent-policy-delete", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "success": { - "type": "boolean" - } - }, - "required": ["id", "success"] - } - }, - "examples": { - "success": { - "value": [ - { - "id": "df7d2540-5a47-11ea-80da-89b5a66da347", - "success": true - } - ] - }, - "fail": { - "value": [ - { - "id": "df7d2540-5a47-11ea-80da-89b5a66da347", - "success": false - } - ] - } - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agentPolicyIds": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "examples": { - "example-1": { - "value": { - "agentPolicyIds": ["df7d2540-5a47-11ea-80da-89b5a66da347"] - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "parameters": [] - }, - "/package_policies": { - "get": { - "summary": "PackagePolicies - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PackagePolicy" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["items"] - }, - "examples": { - "example-1": { - "value": { - "items": [ - { - "id": "5d273cf0-5a44-11ea-80da-89b5a66da347", - "use_output": "default", - "inputs": [ - { - "type": "docker/metrics", - "streams": [ - { - "metricset": "status", - "dataset": "docker.status" - } - ] - }, - { - "type": "logs", - "streams": [ - { - "paths": ["/var/log/hello1.log", "/var/log/hello2.log"] - } - ] - } - ] - }, - { - "id": "66490980-5a44-11ea-80da-89b5a66da347", - "namespace": "testing", - "use_output": "default", - "inputs": [ - { - "type": "apache/metrics", - "streams": [ - { - "enabled": true, - "metricset": "info" - } - ] - } - ] - }, - { - "id": "df1ccae0-5a49-11ea-94a6-81affd263f47", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "f96a09d0-5a49-11ea-94a6-81affd263f47", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "9ca403a0-5a66-11ea-9468-c911a41ab4f5", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "27925980-5a44-11ea-80da-89b5a66da347", - "enabled": true, - "title": "UPDATED title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "streams": [ - { - "paths": ["/var/log/nginx/access.log"], - "dataset": "nginx.acccess", - "enabled": true - }, - { - "paths": ["/var/log/nginx/error.log"], - "dataset": "nginx.error", - "enabled": true - } - ], - "type": "logs" - }, - { - "streams": [ - { - "metricset": "stub_status", - "id": "id string", - "dataset": "nginx.stub_status", - "enabled": true - } - ], - "type": "nginx/metrics" - } - ] - } - ], - "total": 6, - "page": 1, - "perPage": 20 - } - } - } - } - } - } - }, - "operationId": "get-packagePolicies", - "security": [], - "parameters": [] - }, - "parameters": [], - "post": { - "summary": "PackagePolicies - Create", - "operationId": "post-packagePolicies", - "responses": { - "200": { - "description": "OK" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewPackagePolicy" - }, - "examples": { - "example-1": { - "value": { - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/package_policies/{packagePolicyId}": { - "get": { - "summary": "PackagePolicies - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/PackagePolicy" - } - }, - "required": ["item"] - } - } - } - } - }, - "operationId": "get-packagePolicies-packagePolicyId" - }, - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "packagePolicyId", - "in": "path", - "required": true - } - ], - "put": { - "summary": "PackagePolicies - Update", - "operationId": "put-packagePolicies-packagePolicyId", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/PackagePolicy" - }, - "sucess": { - "type": "boolean" - } - }, - "required": ["item", "sucess"] - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agents/setup": { - "get": { - "summary": "Agents setup - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": ["isInitialized"] - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - }, - "failure": { - "value": { - "isInitialized": false - } - } - } - } - } - } - }, - "operationId": "get-agents-setup", - "security": [ - { - "basicAuth": [] - } - ] - }, - "post": { - "summary": "Agents setup - Create", - "operationId": "post-agents-setup", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": ["isInitialized"] - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - } - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "admin_username": { - "type": "string" - }, - "admin_password": { - "type": "string" - } - }, - "required": ["admin_username", "admin_password"] - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/epm/packages/{pkgkey}": { - "get": { - "summary": "EPM - Packages - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "allOf": [ - { - "properties": { - "response": { - "$ref": "#/components/schemas/PackageInfo" - } - } - }, - { - "properties": { - "status": { - "type": "string", - "enum": ["installed", "not_installed"] - }, - "savedObject": { - "type": "string" - } - }, - "required": ["status", "savedObject"] - } - ] - }, - "examples": { - "example-1": { - "value": { - "response": { - "name": "coredns", - "title": "CoreDNS", - "version": "1.0.1", - "readme": "/package/coredns-1.0.1/docs/README.md", - "description": "CoreDNS logs and metrics integration.\nThe CoreDNS integrations allows to gather logs and metrics from the CoreDNS DNS server to get better insights.\n", - "type": "integration", - "categories": ["logs", "metrics"], - "requirement": { - "kibana": { - "versions": ">6.7.0" - } - }, - "icons": [ - { - "path": "/package/coredns-1.0.1/img/icon.png", - "src": "/img/icon.png", - "size": "1800x1800" - }, - { - "path": "/package/coredns-1.0.1/img/icon.svg", - "src": "/img/icon.svg", - "size": "255x144", - "type": "image/svg+xml" - } - ], - "assets": { - "kibana": { - "dashboard": [ - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "dashboard", - "file": "53aa1f70-443e-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/dashboard/53aa1f70-443e-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "dashboard", - "file": "Metricbeat-CoreDNS-Dashboard-ecs.json", - "path": "coredns-1.0.1/kibana/dashboard/Metricbeat-CoreDNS-Dashboard-ecs.json" - } - ], - "visualization": [ - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "277fc650-67a9-11e9-a534-715561d0bf42.json", - "path": "coredns-1.0.1/kibana/visualization/277fc650-67a9-11e9-a534-715561d0bf42.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "27da53f0-53d5-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/27da53f0-53d5-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "36e08510-53c4-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/36e08510-53c4-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "3ad75810-4429-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/3ad75810-4429-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "4804eaa0-7315-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/4804eaa0-7315-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "57c74300-7308-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/57c74300-7308-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "75743f70-443c-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/75743f70-443c-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "86177430-728d-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/86177430-728d-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "9dc640e0-4432-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/9dc640e0-4432-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "a19df590-53c4-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/a19df590-53c4-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "a58345f0-7298-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/a58345f0-7298-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "cfde7fb0-443d-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/cfde7fb0-443d-11e9-8548-ab7fbe04f038.json" - } - ] - } - }, - "format_version": "1.0.0", - "data_streams": [ - { - "title": "CoreDNS logs", - "name": "log", - "release": "ga", - "type": "logs", - "ingest_pipeline": "pipeline-entry", - "vars": [ - { - "default": ["/var/log/coredns.log"], - "name": "paths", - "type": "textarea" - }, - { - "default": ["coredns"], - "name": "tags", - "type": "text" - } - ], - "package": "coredns" - }, - { - "title": "CoreDNS stats metrics", - "name": "stats", - "release": "ga", - "type": "metrics", - "vars": [ - { - "default": ["http://localhost:9153"], - "description": "CoreDNS hosts", - "name": "hosts", - "required": true - }, - { - "default": "10s", - "description": "Collection period. Valid values: 10s, 5m, 2h", - "name": "period" - }, - { - "name": "username", - "type": "text" - }, - { - "name": "password", - "type": "password" - } - ], - "package": "coredns" - } - ], - "download": "/epr/coredns/coredns-1.0.1.tar.gz", - "path": "/package/coredns-1.0.1", - "status": "installed", - "savedObject": { - "id": "coredns-1.0.1", - "type": "epm-package", - "updated_at": "2020-02-27T16:25:43.652Z", - "version": "WzU2LDFd", - "attributes": { - "installed": [ - { - "id": "53aa1f70-443e-11e9-8548-ab7fbe04f038", - "type": "dashboard" - }, - { - "id": "Metricbeat-CoreDNS-Dashboard-ecs", - "type": "dashboard" - }, - { - "id": "75743f70-443c-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "36e08510-53c4-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "277fc650-67a9-11e9-a534-715561d0bf42", - "type": "visualization" - }, - { - "id": "cfde7fb0-443d-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "a19df590-53c4-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "a58345f0-7298-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "9dc640e0-4432-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "3ad75810-4429-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "57c74300-7308-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "27da53f0-53d5-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "86177430-728d-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "4804eaa0-7315-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "logs-log-1.0.1-pipeline-plaintext", - "type": "ingest-pipeline" - }, - { - "id": "logs-log-1.0.1-pipeline-json", - "type": "ingest-pipeline" - }, - { - "id": "logs-log-1.0.1", - "type": "ingest-pipeline" - }, - { - "id": "logs-log", - "type": "index-template" - }, - { - "id": "metrics-stats", - "type": "index-template" - } - ] - }, - "references": [] - } - } - } - }, - "required-package": { - "value": { - "response": { - "format_version": "1.0.0", - "name": "endpoint", - "title": "Elastic Endpoint", - "version": "0.3.0", - "readme": "/package/endpoint/0.3.0/docs/README.md", - "license": "basic", - "description": "This is the Elastic Endpoint package.", - "type": "solution", - "categories": ["security"], - "release": "beta", - "requirement": { - "kibana": { - "versions": ">7.4.0" - } - }, - "icons": [ - { - "path": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg", - "src": "/img/logo-endpoint-64-color.svg", - "size": "16x16", - "type": "image/svg+xml" - } - ], - "assets": { - "kibana": { - "dashboard": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "dashboard", - "file": "826759f0-7074-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/dashboard/826759f0-7074-11ea-9bc8-6b38f4d29a16.json" - } - ], - "map": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "map", - "file": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/map/a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json" - } - ], - "visualization": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "1e525190-7074-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/1e525190-7074-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "55387750-729c-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/55387750-729c-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json" - } - ] - } - }, - "data_streams": [ - { - "id": "endpoint", - "title": "Endpoint Events", - "release": "experimental", - "type": "events", - "package": "endpoint", - "path": "events" - }, - { - "id": "endpoint.metadata", - "title": "Endpoint Metadata", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "metadata" - }, - { - "id": "endpoint.policy", - "title": "Endpoint Policy Response", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "policy" - }, - { - "id": "endpoint.telemetry", - "title": "Endpoint Telemetry", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "telemetry" - } - ], - "packagePolicies": [ - { - "name": "endpoint", - "title": "Endpoint package policy", - "description": "Interact with the endpoint.", - "inputs": null, - "multiple": false - } - ], - "download": "/epr/endpoint/endpoint-0.3.0.tar.gz", - "path": "/package/endpoint/0.3.0", - "latestVersion": "0.3.0", - "removable": false, - "status": "installed", - "savedObject": { - "id": "endpoint", - "type": "epm-packages", - "updated_at": "2020-06-23T21:44:59.319Z", - "version": "Wzk4LDFd", - "attributes": { - "installed": [ - { - "id": "826759f0-7074-11ea-9bc8-6b38f4d29a16", - "type": "dashboard" - }, - { - "id": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1e525190-7074-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "55387750-729c-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16", - "type": "map" - }, - { - "id": "events-endpoint", - "type": "index-template" - }, - { - "id": "metrics-endpoint.metadata", - "type": "index-template" - }, - { - "id": "metrics-endpoint.policy", - "type": "index-template" - }, - { - "id": "metrics-endpoint.telemetry", - "type": "index-template" - } - ], - "es_index_patterns": { - "events": "events-endpoint-*", - "metadata": "metrics-endpoint.metadata-*", - "policy": "metrics-endpoint.policy-*", - "telemetry": "metrics-endpoint.telemetry-*" - }, - "name": "endpoint", - "version": "0.3.0", - "internal": false, - "removable": false - }, - "references": [] - } - } - } - } - } - } - } - } - }, - "operationId": "get-epm-package-pkgkey", - "security": [ - { - "basicAuth": [] - } - ] - }, - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "pkgkey", - "in": "path", - "required": true - } - ], - "post": { - "summary": "EPM - Packages - Install", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["id", "type"] - } - } - }, - "required": ["response"] - } - } - } - } - }, - "operationId": "post-epm-install-pkgkey", - "description": "", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "delete": { - "summary": "EPM - Packages - Delete", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["id", "type"] - } - } - }, - "required": ["response"] - } - } - } - } - }, - "operationId": "post-epm-delete-pkgkey", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/epm/packages": { - "get": { - "summary": "EPM - Packages - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchResult" - } - }, - "examples": { - "success": { - "value": { - "response": [ - { - "description": "aws Integration", - "download": "/epr/aws/aws-0.0.3.tar.gz", - "icons": [ - { - "path": "/package/aws/0.0.3/img/logo_aws.svg", - "src": "/img/logo_aws.svg", - "title": "logo aws", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "aws", - "path": "/package/aws/0.0.3", - "title": "aws", - "type": "integration", - "version": "0.0.3", - "status": "not_installed" - }, - { - "description": "This is the Elastic Endpoint package.", - "download": "/epr/endpoint/endpoint-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg", - "src": "/img/logo-endpoint-64-color.svg", - "size": "16x16", - "type": "image/svg+xml" - } - ], - "name": "endpoint", - "path": "/package/endpoint/0.1.0", - "title": "Elastic Endpoint", - "type": "solution", - "version": "0.1.0", - "status": "installed", - "savedObject": { - "type": "epm-packages", - "id": "endpoint", - "attributes": { - "installed": [ - { - "id": "826759f0-7074-11ea-9bc8-6b38f4d29a16", - "type": "dashboard" - }, - { - "id": "55387750-729c-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1e525190-7074-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16", - "type": "map" - }, - { - "id": "events-endpoint", - "type": "index-template" - }, - { - "id": "metrics-endpoint", - "type": "index-template" - } - ], - "es_index_patterns": { - "events": "events-endpoint-*", - "metadata": "metrics-endpoint-*" - }, - "name": "endpoint", - "version": "0.1.0", - "internal": false, - "removable": false - }, - "references": [], - "updated_at": "2020-05-15T20:08:11.739Z", - "version": "WzEwOCwxXQ==" - } - }, - { - "description": "The log package should be used to create package policies for all type of logs for which an package doesn't exist yet.\n", - "download": "/epr/log/log-0.9.0.tar.gz", - "icons": [ - { - "path": "/package/log/0.9.0/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "log", - "path": "/package/log/0.9.0", - "title": "Log Package", - "type": "integration", - "version": "0.9.0", - "status": "not_installed" - }, - { - "description": "This integration contains pretty long documentation.\nIt is used to show the different visualisations inside a documentation to test how we handle it.\nThe integration does not contain any assets except the documentation page.\n", - "download": "/epr/longdocs/longdocs-1.0.4.tar.gz", - "icons": [ - { - "path": "/package/longdocs/1.0.4/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "longdocs", - "path": "/package/longdocs/1.0.4", - "title": "Long Docs", - "type": "integration", - "version": "1.0.4", - "status": "not_installed" - }, - { - "description": "This is an integration with only the metrics category.\n", - "download": "/epr/metricsonly/metricsonly-2.0.1.tar.gz", - "icons": [ - { - "path": "/package/metricsonly/2.0.1/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "metricsonly", - "path": "/package/metricsonly/2.0.1", - "title": "Metrics Only", - "type": "integration", - "version": "2.0.1", - "status": "not_installed" - }, - { - "description": "Multiple versions of this integration exist.\n", - "download": "/epr/multiversion/multiversion-1.1.0.tar.gz", - "icons": [ - { - "path": "/package/multiversion/1.1.0/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "multiversion", - "path": "/package/multiversion/1.1.0", - "title": "Multi Version", - "type": "integration", - "version": "1.1.0", - "status": "not_installed" - }, - { - "description": "MySQL Integration", - "download": "/epr/mysql/mysql-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/mysql/0.1.0/img/logo_mysql.svg", - "src": "/img/logo_mysql.svg", - "title": "logo mysql", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "mysql", - "path": "/package/mysql/0.1.0", - "title": "MySQL", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "Nginx Integration", - "download": "/epr/nginx/nginx-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/nginx/0.1.0/img/logo_nginx.svg", - "src": "/img/logo_nginx.svg", - "title": "logo nginx", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "nginx", - "path": "/package/nginx/0.1.0", - "title": "Nginx", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "Redis Integration", - "download": "/epr/redis/redis-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/redis/0.1.0/img/logo_redis.svg", - "src": "/img/logo_redis.svg", - "title": "logo redis", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "redis", - "path": "/package/redis/0.1.0", - "title": "Redis", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "This package is used for defining all the properties of a package, the possible assets etc. It serves as a reference on all the config options which are possible.\n", - "download": "/epr/reference/reference-1.0.0.tar.gz", - "icons": [ - { - "path": "/package/reference/1.0.0/img/icon.svg", - "src": "/img/icon.svg", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "reference", - "path": "/package/reference/1.0.0", - "title": "Reference package", - "type": "integration", - "version": "1.0.0", - "status": "not_installed" - }, - { - "description": "System Integration", - "download": "/epr/system/system-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/system/0.1.0/img/system.svg", - "src": "/img/system.svg", - "title": "system", - "size": "1000x1000", - "type": "image/svg+xml" - } - ], - "name": "system", - "path": "/package/system/0.1.0", - "title": "System", - "type": "integration", - "version": "0.1.0", - "status": "installed", - "savedObject": { - "type": "epm-packages", - "id": "system", - "attributes": { - "installed": [ - { - "id": "c431f410-f9ac-11e9-90e8-1fb18e796788", - "type": "dashboard" - }, - { - "id": "Metricbeat-system-overview-ecs", - "type": "dashboard" - }, - { - "id": "277876d0-fa2c-11e6-bbd3-29c986c96e5a-ecs", - "type": "dashboard" - }, - { - "id": "0d3f2380-fa78-11e6-ae9b-81e5311e8cab-ecs", - "type": "dashboard" - }, - { - "id": "CPU-slash-Memory-per-container-ecs", - "type": "dashboard" - }, - { - "id": "79ffd6e0-faa0-11e6-947f-177f697178b8-ecs", - "type": "dashboard" - }, - { - "id": "Filebeat-syslog-dashboard-ecs", - "type": "dashboard" - }, - { - "id": "5517a150-f9ce-11e6-8115-a7c18106d86a-ecs", - "type": "dashboard" - }, - { - "id": "9c69cad0-f9b0-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "855899e0-1b1c-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "a30871f0-f98f-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "e121b140-fa78-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "f398d2f0-fa77-11e6-ae9b-81e5311e8cab-ecs", - "type": "visualization" - }, - { - "id": "c5e3cf90-4d60-11e7-9a4c-ed99bbcaa42b-ecs", - "type": "visualization" - }, - { - "id": "d3166e80-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "346bb290-fa80-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "Container-Block-IO-ecs", - "type": "visualization" - }, - { - "id": "590a60f0-5d87-11e7-8884-1bb4c3b890e4-ecs", - "type": "visualization" - }, - { - "id": "341ffe70-f9ce-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "System-Navigation-ecs", - "type": "visualization" - }, - { - "id": "089b85d0-1b16-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "99381c80-4d60-11e7-9a4c-ed99bbcaa42b-ecs", - "type": "visualization" - }, - { - "id": "c6f2ffd0-4d17-11e7-a196-69b9a7a020a9-ecs", - "type": "visualization" - }, - { - "id": "d56ee420-fa79-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "1aae9140-1b93-11e7-8ada-3df93aab833e-ecs", - "type": "visualization" - }, - { - "id": "e0f001c0-1b18-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "dc589770-fa2b-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "96976150-4d5d-11e7-aa29-87a97a796de6-ecs", - "type": "visualization" - }, - { - "id": "8c071e20-f999-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "d3f51850-f9b6-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "5c7af030-fa2a-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "e6e639e0-f992-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "bfa5e400-1b16-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "7cdb1330-4d1a-11e7-a196-69b9a7a020a9-ecs", - "type": "visualization" - }, - { - "id": "78b74f30-f9cd-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "Syslog-events-by-hostname-ecs", - "type": "visualization" - }, - { - "id": "3d65d450-a9c3-11e7-af20-67db8aecb295-ecs", - "type": "visualization" - }, - { - "id": "ab2d1e90-1b1a-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "825fdb80-4d1d-11e7-b5f2-2b7c1895bf32-ecs", - "type": "visualization" - }, - { - "id": "26732e20-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "Syslog-hostnames-and-processes-ecs", - "type": "visualization" - }, - { - "id": "522ee670-1b92-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "51164310-fa2b-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "bb3a8720-f991-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "Container-Memory-stats-ecs", - "type": "visualization" - }, - { - "id": "5dd15c00-fa78-11e6-ae9b-81e5311e8cab-ecs", - "type": "visualization" - }, - { - "id": "327417e0-8462-11e7-bab8-bd2f0fb42c54-ecs", - "type": "visualization" - }, - { - "id": "d2e80340-4d5c-11e7-aa29-87a97a796de6-ecs", - "type": "visualization" - }, - { - "id": "19e123b0-4d5a-11e7-aee5-fdc812cc3bec-ecs", - "type": "visualization" - }, - { - "id": "3cec3eb0-f9d3-11e6-8a3e-2b904044ea1d-ecs", - "type": "visualization" - }, - { - "id": "2e224660-1b19-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "12667040-fa80-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "d16bb400-f9cc-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "34f97ee0-1b96-11e7-8ada-3df93aab833e-ecs", - "type": "visualization" - }, - { - "id": "fe064790-1b1f-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "83e12df0-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "4e4bb1e0-1b1b-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "4b254630-f998-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "6b7b9a40-faa1-11e6-86b1-cd7735ff7e23-ecs", - "type": "visualization" - }, - { - "id": "4d546850-1b15-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "Container-CPU-usage-ecs", - "type": "visualization" - }, - { - "id": "b6f321e0-fa25-11e6-bbd3-29c986c96e5a-ecs", - "type": "search" - }, - { - "id": "62439dc0-f9c9-11e6-a747-6121780e0414-ecs", - "type": "search" - }, - { - "id": "8030c1b0-fa77-11e6-ae9b-81e5311e8cab-ecs", - "type": "search" - }, - { - "id": "Syslog-system-logs-ecs", - "type": "search" - }, - { - "id": "eb0039f0-fa7f-11e6-a1df-a78bd7504d38-ecs", - "type": "search" - }, - { - "id": "logs-system.auth-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.auth-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.syslog-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.syslog-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.auth", - "type": "index-template" - }, - { - "id": "metrics-system.core", - "type": "index-template" - }, - { - "id": "metrics-system.cpu", - "type": "index-template" - }, - { - "id": "metrics-system.diskio", - "type": "index-template" - }, - { - "id": "metrics-system.entropy", - "type": "index-template" - }, - { - "id": "metrics-system.filesystem", - "type": "index-template" - }, - { - "id": "metrics-system.fsstat", - "type": "index-template" - }, - { - "id": "metrics-system.load", - "type": "index-template" - }, - { - "id": "metrics-system.memory", - "type": "index-template" - }, - { - "id": "metrics-system.network", - "type": "index-template" - }, - { - "id": "metrics-system.network_summary", - "type": "index-template" - }, - { - "id": "metrics-system.process", - "type": "index-template" - }, - { - "id": "metrics-system.process_summary", - "type": "index-template" - }, - { - "id": "metrics-system.raid", - "type": "index-template" - }, - { - "id": "metrics-system.service", - "type": "index-template" - }, - { - "id": "metrics-system.socket", - "type": "index-template" - }, - { - "id": "metrics-system.socket_summary", - "type": "index-template" - }, - { - "id": "logs-system.syslog", - "type": "index-template" - }, - { - "id": "metrics-system.uptime", - "type": "index-template" - }, - { - "id": "metrics-system.users", - "type": "index-template" - } - ], - "es_index_patterns": { - "auth": "logs-system.auth-*", - "core": "metrics-system.core-*", - "cpu": "metrics-system.cpu-*", - "diskio": "metrics-system.diskio-*", - "entropy": "metrics-system.entropy-*", - "filesystem": "metrics-system.filesystem-*", - "fsstat": "metrics-system.fsstat-*", - "load": "metrics-system.load-*", - "memory": "metrics-system.memory-*", - "network": "metrics-system.network-*", - "network_summary": "metrics-system.network_summary-*", - "process": "metrics-system.process-*", - "process_summary": "metrics-system.process_summary-*", - "raid": "metrics-system.raid-*", - "service": "metrics-system.service-*", - "socket": "metrics-system.socket-*", - "socket_summary": "metrics-system.socket_summary-*", - "syslog": "logs-system.syslog-*", - "uptime": "metrics-system.uptime-*", - "users": "metrics-system.users-*" - }, - "name": "system", - "version": "0.1.0", - "internal": false, - "removable": false - }, - "references": [], - "updated_at": "2020-05-15T20:08:08.708Z", - "version": "Wzk4LDFd" - } - }, - { - "description": "This package contains a yaml pipeline.\n", - "download": "/epr/yamlpipeline/yamlpipeline-1.0.0.tar.gz", - "name": "yamlpipeline", - "path": "/package/yamlpipeline/1.0.0", - "title": "Yaml Pipeline package", - "type": "integration", - "version": "1.0.0", - "status": "not_installed" - } - ] - } - } - } - } - } - } - }, - "operationId": "get-epm-list" - }, - "parameters": [] - }, - "/epm/categories": { - "get": { - "summary": "EPM - Categories", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "count": { - "type": "number" - } - }, - "required": ["id", "title", "count"] - } - } - } - } - } - }, - "operationId": "get-epm-categories" - } - }, - "/fleet/agents": { - "get": { - "summary": "Fleet - Agent - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "type": "object" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["list", "total", "page", "perPage"] - }, - "examples": { - "example-1": { - "value": { - "list": [ - { - "id": "205661d0-5e53-11ea-ad31-4f31c06bd9a4", - "active": true, - "policy_id": "ae556400-5e39-11ea-8b49-f9747e466f7b", - "type": "PERMANENT", - "enrolled_at": "2020-03-04T20:02:50.605Z", - "user_provided_metadata": { - "dev_agent_version": "0.0.1", - "region": "us-east" - }, - "local_metadata": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "actions": [ - { - "data": "{\"config\":{\"id\":\"ae556400-5e39-11ea-8b49-f9747e466f7b\",\"outputs\":{\"default\":{\"type\":\"elasticsearch\",\"hosts\":[\"http://localhost:9200\"],\"api_key\":\"\",\"api_token\":\"6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw\"}},\"packagePolicies\":[]}}", - "created_at": "2020-03-04T20:02:56.149Z", - "id": "6a95c00a-d76d-4931-97c3-0bf935272d7d", - "type": "POLICY_CHANGE" - } - ], - "access_api_key_id": "6Mkkp3ABz7e_XRqrzLNJ", - "default_api_key": "6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw", - "current_error_events": [], - "last_checkin": "2020-03-04T20:03:05.700Z", - "status": "online" - } - ], - "total": 1, - "page": 1, - "perPage": 20 - } - } - } - } - } - } - }, - "operationId": "get-fleet-agents", - "security": [ - { - "basicAuth": [] - } - ] - } - }, - "/fleet/agents/{agentId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Agent - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "type": "object" - } - }, - "required": ["item"] - } - } - } - } - }, - "operationId": "get-fleet-agents-agentId" - }, - "put": { - "summary": "Fleet - Agent - Update", - "tags": [], - "responses": {}, - "operationId": "put-fleet-agents-agentId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "delete": { - "summary": "Fleet - Agent - Delete", - "tags": [], - "responses": {}, - "operationId": "delete-fleet-agents-agentId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/agents/{agentId}/events": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Agent - Events", - "tags": [], - "responses": {}, - "operationId": "get-fleet-agents-agentId-events" - } - }, - "/fleet/agents/{agentId}/checkin": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Check In", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["checkin"] - }, - "actions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "agent_id": { - "type": "string" - }, - "data": { - "type": "object" - }, - "id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "type": { - "type": "string" - } - }, - "required": ["agent_id", "data", "id", "created_at", "type"] - } - } - } - }, - "examples": { - "success": { - "value": { - "action": "checkin", - "actions": [ - { - "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", - "type": "POLICY_CHANGE", - "data": { - "config": { - "id": "2fe89350-a5e0-11ea-a587-5f886c8a849f", - "outputs": { - "default": { - "type": "elasticsearch", - "hosts": ["http://localhost:9200"], - "api_key": "Z-XkgHIBvwtjzIKtSCTh:AejRqdKpQx6z-6dqSI1LHg" - } - }, - "packagePolicies": [ - { - "id": "33d6bd70-a5e0-11ea-a587-5f886c8a849f", - "name": "system-1", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [ - { - "type": "logs", - "enabled": true, - "streams": [ - { - "id": "logs-system.auth", - "enabled": true, - "dataset": "system.auth", - "paths": ["/var/log/auth.log*", "/var/log/secure*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - }, - { - "id": "logs-system.syslog", - "enabled": true, - "dataset": "system.syslog", - "paths": ["/var/log/messages*", "/var/log/syslog*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - ] - }, - { - "type": "system/metrics", - "enabled": true, - "streams": [ - { - "id": "system/metrics-system.core", - "enabled": true, - "dataset": "system.core", - "metricsets": ["core"], - "core.metrics": "percentages" - }, - { - "id": "system/metrics-system.cpu", - "enabled": true, - "dataset": "system.cpu", - "metricsets": ["cpu"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.diskio", - "enabled": true, - "dataset": "system.diskio", - "metricsets": ["diskio"] - }, - { - "id": "system/metrics-system.entropy", - "enabled": true, - "dataset": "system.entropy", - "metricsets": ["entropy"] - }, - { - "id": "system/metrics-system.filesystem", - "enabled": true, - "dataset": "system.filesystem", - "metricsets": ["filesystem"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - }, - { - "id": "system/metrics-system.fsstat", - "enabled": true, - "dataset": "system.fsstat", - "metricsets": ["fsstat"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - }, - { - "id": "system/metrics-system.load", - "enabled": true, - "dataset": "system.load", - "metricsets": ["load"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.memory", - "enabled": true, - "dataset": "system.memory", - "metricsets": ["memory"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.network", - "enabled": true, - "dataset": "system.network", - "metricsets": ["network"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.network_summary", - "enabled": true, - "dataset": "system.network_summary", - "metricsets": ["network_summary"] - }, - { - "id": "system/metrics-system.process", - "enabled": true, - "dataset": "system.process", - "metricsets": ["process"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.process_summary", - "enabled": true, - "dataset": "system.process_summary", - "metricsets": ["process_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.raid", - "enabled": true, - "dataset": "system.raid", - "metricsets": ["raid"] - }, - { - "id": "system/metrics-system.service", - "enabled": true, - "dataset": "system.service", - "metricsets": ["service"] - }, - { - "id": "system/metrics-system.socket", - "enabled": true, - "dataset": "system.socket", - "metricsets": ["socket"] - }, - { - "id": "system/metrics-system.socket_summary", - "enabled": true, - "dataset": "system.socket_summary", - "metricsets": ["socket_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.uptime", - "enabled": true, - "dataset": "system.uptime", - "metricsets": ["uptime"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "processes": ".*" - }, - { - "id": "system/metrics-system.users", - "enabled": true, - "dataset": "system.users", - "metricsets": ["users"] - } - ] - } - ], - "package": { - "name": "system", - "version": "0.1.0" - } - }, - { - "id": "fdb1fea0-a5f6-11ea-ad52-534e35d3cd6f", - "name": "endpoint-1", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [], - "package": { - "name": "endpoint", - "version": "0.2.0" - } - }, - { - "id": "2d792280-a5f7-11ea-ad52-534e35d3cd6f", - "name": "endpoint-2", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [], - "package": { - "name": "endpoint", - "version": "0.2.0" - } - } - ], - "revision": 4, - "settings": { - "monitoring": { - "use_output": "default", - "enabled": true, - "logs": true, - "metrics": true - } - } - } - }, - "id": "51c6ad1e-a9c0-4c70-80da-99a5c51eedaf", - "created_at": "2020-06-04T19:52:24.667Z" - } - ] - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-agentId-checkin", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "security": [ - { - "Access API Key": [] - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "local_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NewAgentEvent" - } - } - } - }, - "examples": { - "stoped to starting": { - "value": { - "events": [ - { - "type": "STATE", - "subtype": "STARTING", - "message": "state changed from STOPPED to STARTING", - "timestamp": "2019-10-01T13:42:54.323Z", - "payload": {}, - "agent_id": "bee40627-8cbd-45df-add9-98c390f9db10" - } - ] - } - }, - "running": { - "value": { - "events": [ - { - "type": "STATE", - "subtype": "RUNNING", - "message": "state changed from STOPPED to RUNNING", - "timestamp": "2020-05-26T20:44:57.480Z", - "payload": { - "random": "data", - "state": "RUNNING", - "previous_state": "STOPPED" - }, - "agent_id": "bee40627-8cbd-45df-add9-98c390f9db10" - } - ] - } - } - } - } - } - } - } - }, - "/fleet/agents/{agentId}/acks": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Acks", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["acks"] - } - }, - "required": ["action"] - }, - "examples": { - "success": { - "value": { - "action": "checkin" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-agentId-acks", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - }, - "examples": { - "example-1": { - "value": { - "events": [ - { - "type": "ACTION_RESULT", - "subtype": "CONFIG", - "timestamp": "2019-01-04T14:32:03.36764-05:00", - "action_id": "51c6ad1e-a9c0-4c70-80da-99a5c51eedaf", - "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", - "message": "acknowledge" - } - ] - } - } - } - } - } - } - } - }, - "/fleet/agents/enroll": { - "post": { - "summary": "Fleet - Agent - Enroll", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "item": { - "$ref": "#/components/schemas/Agent" - } - } - }, - "examples": { - "success": { - "value": { - "action": "created", - "item": { - "id": "8086fb1a-72ca-4a67-8533-09300c1639fa", - "active": true, - "policy_id": "2fe89350-a5e0-11ea-a587-5f886c8a849f", - "type": "PERMANENT", - "enrolled_at": "2020-06-04T13:03:57.856Z", - "user_provided_metadata": { - "dev_agent_version": "0.0.1", - "region": "us-east" - }, - "local_metadata": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "current_error_events": [], - "access_api_key": "cU9KdWYzSUJ2d3RqeklLdFdnNF86ZW05ZjFrMThUWW1GRW13OHMwRGZvdw==", - "status": "error" - } - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-enroll", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["PERMANENT", "EPHEMERAL", "TEMPORARY"] - }, - "shared_id": { - "type": "string" - }, - "metadata": { - "type": "object", - "required": ["local", "user_provided"], - "properties": { - "local": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "user_provided": { - "$ref": "#/components/schemas/AgentMetadata" - } - } - } - }, - "required": ["type", "metadata"] - }, - "examples": { - "good": { - "value": { - "type": "PERMANENT", - "metadata": { - "local": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "user_provided": { - "dev_agent_version": "0.0.1", - "region": "us-east" - } - } - } - } - } - } - } - }, - "security": [ - { - "Enrollment API Key": [] - } - ] - } - }, - "/fleet/agents/{agentId}/unenroll": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Unenroll", - "tags": [], - "responses": {}, - "operationId": "post-fleet-agents-unenroll", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "force": { "type": "boolean" } - } - }, - "examples": { - "example-1": { - "value": { - "force": true - } - } - } - } - } - } - } - }, - "/fleet/agents/{agentId}/upgrade": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Upgrade", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "success": { - "value": {} - } - } - } - } - }, - "400": { - "description": "BAD REQUEST", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "bad request not upgradeable": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "agent d133b07d-5c2b-42f0-8e6b-bbae53bdce88 is not upgradeable" - } - }, - "bad request kibana version": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade agent to 8.0.0 because it is different than the installed kibana version 7.9.10" - } - }, - "bad request agent unenrolling": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade an unenrolling or unenrolled agent" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-upgrade", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples":{ - "version":{ - "value": { - "version": "8.0.0" - } - }, - "version and source_uri":{ - "value": { - "version": "8.0.0", - "source_uri": "http://localhost:8000" - } - } - } - } - } - } - } - }, - "/fleet/agents/bulk_upgrade": { - "post": { - "summary": "Fleet - Agent - Bulk Upgrade", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkUpgradeAgents" - }, - "examples": { - "success": { - "value": {} - } - } - } - } - }, - "400": { - "description": "BAD REQUEST", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "bad request kibana version": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade agent to 8.0.0 because it is different than the installed kibana version 7.9.10" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-bulk-upgrade", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkUpgradeAgents" - }, - "examples":{ - "version":{ - "value": { - "version": "8.0.0" - } - }, - "version and source_uri":{ - "value": { - "version": "8.0.0", - "source_uri": "http://localhost:8000" - } - } - } - } - } - } - } - }, - "/fleet/agent-status": { - "get": { - "summary": "Fleet - Agent - Status for policy", - "tags": [], - "responses": {}, - "operationId": "get-fleet-agent-status", - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "policyId", - "in": "query", - "required": false - } - ] - } - }, - "/fleet/enrollment-api-keys": { - "get": { - "summary": "Enrollment - List", - "tags": [], - "responses": {}, - "operationId": "get-fleet-enrollment-api-keys", - "parameters": [] - }, - "post": { - "summary": "Enrollment - Create", - "tags": [], - "responses": {}, - "operationId": "post-fleet-enrollment-api-keys", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/enrollment-api-keys/{keyId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "keyId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Enrollment - Info", - "tags": [], - "responses": {}, - "operationId": "get-fleet-enrollment-api-keys-keyId" - }, - "delete": { - "summary": "Enrollment - Delete", - "tags": [], - "responses": {}, - "operationId": "delete-fleet-enrollment-api-keys-keyId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/setup": { - "post": { - "summary": "Ingest Manager - Setup", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - } - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - } - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "examples": {} - } - } - } - }, - "operationId": "post-setup", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/install/{osType}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "osType", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Get OS install script", - "tags": [], - "responses": {}, - "operationId": "get-fleet-install-osType" - } - } - }, - "components": { - "schemas": { - "AgentPolicy": { - "allOf": [ - { - "$ref": "#/components/schemas/NewAgentPolicy" - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["active", "inactive"] - }, - "packagePolicies": { - "oneOf": [ - { - "items": { - "type": "string" - } - }, - { - "items": { - "$ref": "#/components/schemas/PackagePolicy" - } - } - ], - "type": "array" - }, - "updated_on": { - "type": "string", - "format": "date-time" - }, - "updated_by": { - "type": "string" - }, - "revision": { - "type": "number" - }, - "agents": { - "type": "number" - } - }, - "required": ["id", "status"] - } - ] - }, - "PackagePolicy": { - "title": "PackagePolicy", - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "revision": { - "type": "number" - }, - "inputs": { - "type": "array", - "items": {} - } - }, - "required": ["id", "revision"] - }, - { - "$ref": "#/components/schemas/NewPackagePolicy" - } - ], - "x-examples": { - "example-1": {} - } - }, - "NewAgentPolicy": { - "title": "NewAgentPolicy", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "description": { - "type": "string" - } - } - }, - "NewPackagePolicy": { - "title": "NewPackagePolicy", - "type": "object", - "x-examples": { - "example-1": { - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - } - }, - "description": "", - "properties": { - "enabled": { - "type": "boolean" - }, - "package": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": ["name", "version", "title"] - }, - "namespace": { - "type": "string" - }, - "output_id": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "processors": { - "type": "array", - "items": { - "type": "string" - } - }, - "streams": { - "type": "array", - "items": {} - }, - "config": { - "type": "object" - }, - "vars": { - "type": "object" - } - }, - "required": ["type", "enabled", "streams"] - } - }, - "policy_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["output_id", "inputs", "policy_id", "name"] - }, - "PackageInfo": { - "title": "PackageInfo", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "version": { - "type": "string" - }, - "readme": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string" - }, - "categories": { - "type": "array", - "items": { - "type": "string" - } - }, - "requirement": { - "oneOf": [ - { - "properties": { - "kibana": { - "type": "object", - "properties": { - "versions": { - "type": "string" - } - } - } - } - }, - { - "properties": { - "elasticsearch": { - "type": "object", - "properties": { - "versions": { - "type": "string" - } - } - } - } - } - ], - "type": "object" - }, - "screenshots": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "path": { - "type": "string" - }, - "title": { - "type": "string" - }, - "size": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["src", "path"] - } - }, - "icons": { - "type": "array", - "items": { - "type": "string" - } - }, - "assets": { - "type": "array", - "items": { - "type": "string" - } - }, - "internal": { - "type": "boolean" - }, - "format_version": { - "type": "string" - }, - "data_streams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "name": { - "type": "string" - }, - "release": { - "type": "string" - }, - "ingeset_pipeline": { - "type": "string" - }, - "vars": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "default": { - "type": "string" - } - }, - "required": ["name", "default"] - } - }, - "type": { - "type": "string" - }, - "package": { - "type": "string" - } - }, - "required": ["title", "name", "release", "ingeset_pipeline", "type", "package"] - } - }, - "download": { - "type": "string" - }, - "path": { - "type": "string" - }, - "removable": { - "type": "boolean" - } - }, - "required": [ - "name", - "title", - "version", - "description", - "type", - "categories", - "requirement", - "assets", - "format_version", - "download", - "path" - ] - }, - "SearchResult": { - "title": "SearchResult", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "download": { - "type": "string" - }, - "icons": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "title": { - "type": "string" - }, - "type": { - "type": "string" - }, - "version": { - "type": "string" - }, - "status": { - "type": "string" - }, - "savedObject": { - "type": "object" - } - }, - "required": [ - "description", - "download", - "icons", - "name", - "path", - "title", - "type", - "version", - "status" - ] - }, - "AgentStatus": { - "type": "string", - "title": "AgentStatus", - "enum": ["offline", "error", "online", "inactive", "warning"] - }, - "Agent": { - "title": "Agent", - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/AgentType" - }, - "active": { - "type": "boolean" - }, - "enrolled_at": { - "type": "string" - }, - "unenrolled_at": { - "type": "string" - }, - "unenrollment_started_at": { - "type": "string" - }, - "shared_id": { - "type": "string" - }, - "access_api_key_id": { - "type": "string" - }, - "default_api_key_id": { - "type": "string" - }, - "policy_id": { - "type": "string" - }, - "policy_revision": { - "type": ["number", "null"] - }, - "last_checkin": { - "type": "string" - }, - "user_provided_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "local_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "id": { - "type": "string" - }, - "current_error_events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentEvent" - } - }, - "access_api_key": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/AgentStatus" - }, - "default_api_key": { - "type": "string" - } - }, - "required": ["type", "active", "enrolled_at", "id", "current_error_events", "status"] - }, - "AgentType": { - "type": "string", - "title": "AgentType", - "enum": ["PERMANENT", "EPHEMERAL", "TEMPORARY"] - }, - "AgentMetadata": { - "title": "AgentMetadata", - "type": "object" - }, - "NewAgentEvent": { - "title": "NewAgentEvent", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["STATE", "ERROR", "ACTION_RESULT", "ACTION"] - }, - "subtype": { - "type": "string", - "enum": [ - "RUNNING", - "STARTING", - "IN_PROGRESS", - "CONFIG", - "FAILED", - "STOPPING", - "STOPPED", - "DEGRADED", - "DATA_DUMP", - "ACKNOWLEDGED", - "UNKNOWN" - ] - }, - "timestamp": { - "type": "string" - }, - "message": { - "type": "string" - }, - "payload": { - "type": "string" - }, - "agent_id": { - "type": "string" - }, - "policy_id": { - "type": "string" - }, - "stream_id": { - "type": "string" - }, - "action_id": { - "type": "string" - } - }, - "required": ["type", "subtype", "timestamp", "message", "agent_id"] - }, - "AgentEvent": { - "title": "AgentEvent", - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"] - }, - { - "$ref": "#/components/schemas/NewAgentEvent" - } - ] - }, - "AccessApiKey": { - "type": "string", - "title": "AccessApiKey", - "format": "byte" - }, - "EnrollmentApiKey": { - "type": "string", - "title": "EnrollmentApiKey", - "format": "byte" - }, - "UpgradeAgent":{ - "title": "UpgradeAgent", - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - } - }, - "required": ["version"] - } - ] - }, - "BulkUpgradeAgents":{ - "title": "BulkUpgradeAgents", - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "agents":{ - "type": "array", - "items":{ - "type": "string" - } - } - }, - "required": ["version", "agents"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - }, - "agents":{ - "type": "array", - "items":{ - "type": "string" - } - } - }, - "required": ["version", "agents"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - }, - "agents":{ - "type": "string" - } - }, - "required": ["version", "agents"] - } - ] - } - }, - - "parameters": { - "pageSizeParam": { - "name": "perPage", - "in": "query", - "description": "The number of items to return", - "required": false, - "schema": { - "type": "integer", - "default": 50 - } - }, - "pageIndexParam": { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 1 - } - }, - "kueryParam": { - "name": "kuery", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - "xsrfHeader": { - "schema": { - "type": "string" - }, - "in": "header", - "name": "kbn-xsrf", - "required": true - } - }, - "securitySchemes": { - "basicAuth": { - "type": "http", - "scheme": "basic" - }, - "Enrollment API Key": { - "name": "Authorization", - "type": "apiKey", - "in": "header", - "description": "e.g. Authorization: ApiKey base64EnrollmentApiKey" - }, - "Access API Key": { - "name": "Authorization", - "type": "apiKey", - "in": "header", - "description": "e.g. Authorization: ApiKey base64AccessApiKey" - } - } - }, - "security": [ - { - "basicAuth": [] - } - ] -} diff --git a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts index cb087a3b8f805..ca0fcd3c52c9a 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts @@ -6,7 +6,17 @@ import { isAgentUpgradeable } from './is_agent_upgradeable'; import { Agent } from '../types/models/agent'; -const getAgent = (version: string, upgradeable: boolean): Agent => { +const getAgent = ({ + version, + upgradeable = false, + unenrolling = false, + unenrolled = false, +}: { + version: string; + upgradeable?: boolean; + unenrolling?: boolean; + unenrolled?: boolean; +}): Agent => { const agent: Agent = { id: 'de9006e1-54a7-4320-b24e-927e6fe518a8', active: true, @@ -76,25 +86,53 @@ const getAgent = (version: string, upgradeable: boolean): Agent => { if (upgradeable) { agent.local_metadata.elastic.agent.upgradeable = true; } + if (unenrolling) { + agent.unenrollment_started_at = '2020-10-01T14:43:27.255Z'; + } + if (unenrolled) { + agent.unenrolled_at = '2020-10-01T14:43:27.255Z'; + } return agent; }; describe('Ingest Manager - isAgentUpgradeable', () => { it('returns false if agent reports not upgradeable with agent version < kibana version', () => { - expect(isAgentUpgradeable(getAgent('7.9.0', false), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '7.9.0' }), '8.0.0')).toBe(false); }); it('returns false if agent reports not upgradeable with agent version > kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', false), '7.9.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0' }), '7.9.0')).toBe(false); }); it('returns false if agent reports not upgradeable with agent version === kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', false), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0' }), '8.0.0')).toBe(false); }); it('returns false if agent reports upgradeable, with agent version === kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', true), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0', upgradeable: true }), '8.0.0')).toBe( + false + ); }); it('returns false if agent reports upgradeable, with agent version > kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', true), '7.9.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0', upgradeable: true }), '7.9.0')).toBe( + false + ); + }); + it('returns false if agent reports upgradeable, but agent is unenrolling', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, unenrolling: true }), + '8.0.0' + ) + ).toBe(false); + }); + it('returns false if agent reports upgradeable, but agent is unenrolled', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, unenrolled: true }), + '8.0.0' + ) + ).toBe(false); }); it('returns true if agent reports upgradeable, with agent version < kibana version', () => { - expect(isAgentUpgradeable(getAgent('7.9.0', true), '8.0.0')).toBe(true); + expect(isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0')).toBe( + true + ); }); }); diff --git a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts index 5f96e108e6184..7b59fb7b22825 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts @@ -13,6 +13,7 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string) { } else { return false; } + if (agent.unenrollment_started_at || agent.unenrolled_at) return false; const kibanaVersionParsed = semver.parse(kibanaVersion); const agentVersionParsed = semver.parse(agentVersion); if (!agentVersionParsed || !kibanaVersionParsed) return false; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index ea7fd60d1fa3f..2ec9d7be6c882 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -44,6 +44,11 @@ export enum ElasticsearchAssetType { transform = 'transform', } +export enum DataType { + logs = 'logs', + metrics = 'metrics', +} + export enum AgentAssetType { input = 'input', } diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index ad2eecc0bb057..e13c023d0d11a 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -25,8 +25,8 @@ export const config: PluginConfigDescriptor = { agents: true, }, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('xpack.ingestManager.fleet', 'xpack.fleet.agents'), renameFromRoot('xpack.ingestManager', 'xpack.fleet'), + renameFromRoot('xpack.fleet.fleet', 'xpack.fleet.agents'), ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts index d247b35c089e5..f9a8b63bb83ad 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts @@ -6,6 +6,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { agentPolicyService } from './agent_policy'; +import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { Output } from '../types'; function getSavedObjectMock(agentPolicyAttributes: any) { @@ -59,7 +60,42 @@ jest.mock('./output', () => { }; }); +jest.mock('./agent_policy_update'); + +function getAgentPolicyUpdateMock() { + return (agentPolicyUpdateEventHandler as unknown) as jest.Mock< + typeof agentPolicyUpdateEventHandler + >; +} + describe('agent policy', () => { + beforeEach(() => { + getAgentPolicyUpdateMock().mockClear(); + }); + describe('bumpRevision', () => { + it('should call agentPolicyUpdateEventHandler with updated event once', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + await agentPolicyService.bumpRevision(soClient, 'agent-policy'); + + expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); + }); + }); + + describe('bumpAllAgentPolicies', () => { + it('should call agentPolicyUpdateEventHandler with updated event once', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + await agentPolicyService.bumpAllAgentPolicies(soClient); + + expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); + }); + }); + describe('getFullAgentPolicy', () => { it('should return a policy without monitoring if monitoring is not enabled', async () => { const soClient = getSavedObjectMock({ diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index f1dcc7e5d6c99..44ddb6add8476 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -128,25 +128,26 @@ class AgentPolicyService { public async requireUniqueName( soClient: SavedObjectsClientContract, - { name, namespace }: Pick + givenPolicy: { id?: string; name: string; namespace: string } ) { const results = await soClient.find({ type: SAVED_OBJECT_TYPE, searchFields: ['namespace', 'name'], - search: `${namespace} + ${escapeSearchQueryPhrase(name)}`, + search: `${givenPolicy.namespace} + ${escapeSearchQueryPhrase(givenPolicy.name)}`, }); - - if (results.total) { - const policies = results.saved_objects; - const isSinglePolicy = policies.length === 1; - const policyList = isSinglePolicy ? policies[0].id : policies.map(({ id }) => id).join(','); - const existClause = isSinglePolicy - ? `Agent Policy '${policyList}' already exists` - : `Agent Policies '${policyList}' already exist`; - - throw new AgentPolicyNameExistsError( - `${existClause} in '${namespace}' namespace with name '${name}'` - ); + const idsWithName = results.total && results.saved_objects.map(({ id }) => id); + if (Array.isArray(idsWithName)) { + const isEditingSelf = givenPolicy.id && idsWithName.includes(givenPolicy.id); + if (!givenPolicy.id || !isEditingSelf) { + const isSinglePolicy = idsWithName.length === 1; + const existClause = isSinglePolicy + ? `Agent Policy '${idsWithName[0]}' already exists` + : `Agent Policies '${idsWithName.join(',')}' already exist`; + + throw new AgentPolicyNameExistsError( + `${existClause} in '${givenPolicy.namespace}' namespace with name '${givenPolicy.name}'` + ); + } } } @@ -237,6 +238,7 @@ class AgentPolicyService { ): Promise { if (agentPolicy.name && agentPolicy.namespace) { await this.requireUniqueName(soClient, { + id, name: agentPolicy.name, namespace: agentPolicy.namespace, }); @@ -297,8 +299,6 @@ class AgentPolicyService { ): Promise { const res = await this._update(soClient, id, {}, options?.user); - await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id); - return res; } public async bumpAllAgentPolicies( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts index 612ebf9c11ab3..2e77f069b0956 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts @@ -9,6 +9,8 @@ import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../t import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; import { getAgents, listAllAgents } from './crud'; +import { isAgentUpgradeable } from '../../../common/services'; +import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, @@ -69,7 +71,8 @@ export async function sendUpgradeAgentsActions( version: string; } ) { - // Filter out agents currently unenrolling, agents unenrolled + const kibanaVersion = appContextService.getKibanaVersion(); + // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options ? await getAgents(soClient, options.agentIds) @@ -79,9 +82,7 @@ export async function sendUpgradeAgentsActions( showInactive: false, }) ).agents; - const agentsToUpdate = agents.filter( - (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at - ); + const agentsToUpdate = agents.filter((agent) => isAgentUpgradeable(agent, kibanaVersion)); const now = new Date().toISOString(); const data = { version: options.version, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index e0fea59107c26..8d33180d6262d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -11,6 +11,7 @@ import { TemplateRef, IndexTemplate, IndexTemplateMappings, + DataType, } from '../../../../types'; import { getRegistryDataStreamAssetBaseName } from '../index'; @@ -400,13 +401,6 @@ const updateExistingIndex = async ({ delete mappings.properties.stream; delete mappings.properties.data_stream; - // get the data_stream values from the index template to compose data stream name - const indexMappings = await getIndexMappings(indexName, callCluster); - const dataStream = indexMappings[indexName].mappings.properties.data_stream.properties; - if (!dataStream.type.value || !dataStream.dataset.value || !dataStream.namespace.value) - throw new Error(`data_stream values are missing from the index template ${indexName}`); - const dataStreamName = `${dataStream.type.value}-${dataStream.dataset.value}-${dataStream.namespace.value}`; - // try to update the mappings first try { await callCluster('indices.putMapping', { @@ -416,13 +410,54 @@ const updateExistingIndex = async ({ // if update fails, rollover data stream } catch (err) { try { + // get the data_stream values to compose datastream name + const searchDataStreamFieldsResponse = await callCluster('search', { + index: indexTemplate.index_patterns[0], + body: { + size: 1, + _source: ['data_stream.namespace', 'data_stream.type', 'data_stream.dataset'], + query: { + bool: { + filter: [ + { + exists: { + field: 'data_stream.type', + }, + }, + { + exists: { + field: 'data_stream.dataset', + }, + }, + { + exists: { + field: 'data_stream.namespace', + }, + }, + ], + }, + }, + }, + }); + if (searchDataStreamFieldsResponse.hits.total.value === 0) + throw new Error('data_stream fields are missing from datastream indices'); + const { + dataset, + namespace, + type, + }: { + dataset: string; + namespace: string; + type: DataType; + } = searchDataStreamFieldsResponse.hits.hits[0]._source.data_stream; + const dataStreamName = `${type}-${dataset}-${namespace}`; const path = `/${dataStreamName}/_rollover`; await callCluster('transport.request', { method: 'POST', path, }); } catch (error) { - throw new Error(`cannot rollover data stream ${dataStreamName}`); + throw new Error(`cannot rollover data stream ${error}`); } } // update settings after mappings was successful to ensure @@ -438,14 +473,3 @@ const updateExistingIndex = async ({ throw new Error(`could not update index template settings for ${indexName}`); } }; - -const getIndexMappings = async (indexName: string, callCluster: CallESAsCurrentUser) => { - try { - const indexMappings = await callCluster('indices.getMapping', { - index: indexName, - }); - return indexMappings; - } catch (err) { - throw new Error(`could not get mapping from ${indexName}`); - } -}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 4804701df23a8..4e307e1ac6880 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -12,7 +12,12 @@ import { import * as Registry from '../../registry'; import { loadFieldsFromYaml, Fields, Field } from '../../fields/field'; import { getPackageKeysByStatus } from '../../packages/get'; -import { InstallationStatus, RegistryPackage, CallESAsCurrentUser } from '../../../../types'; +import { + InstallationStatus, + RegistryPackage, + CallESAsCurrentUser, + DataType, +} from '../../../../types'; import { appContextService } from '../../../../services'; interface FieldFormatMap { @@ -69,10 +74,7 @@ export interface IndexPatternField { lang?: string; readFromDocValues: boolean; } -export enum IndexPatternType { - logs = 'logs', - metrics = 'metrics', -} + // TODO: use a function overload and make pkgName and pkgVersion required for install/update // and not for an update removal. or separate out the functions export async function installIndexPatterns( @@ -117,7 +119,7 @@ export async function installIndexPatterns( const packageVersionsInfo = await Promise.all(packageVersionsFetchInfoPromise); // for each index pattern type, create an index pattern - const indexPatternTypes = [IndexPatternType.logs, IndexPatternType.metrics]; + const indexPatternTypes = [DataType.logs, DataType.metrics]; indexPatternTypes.forEach(async (indexPatternType) => { // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern if (!pkgName && installedPackages.length === 0) { @@ -144,7 +146,7 @@ export async function installIndexPatterns( // of all fields from all data streams matching data stream type export const getAllDataStreamFieldsByType = async ( packages: RegistryPackage[], - dataStreamType: IndexPatternType + dataStreamType: DataType ): Promise => { const dataStreamsPromises = packages.reduce>>((acc, pkg) => { if (pkg.data_streams) { @@ -389,7 +391,7 @@ export const ensureDefaultIndices = async (callCluster: CallESAsCurrentUser) => // that no matching indices exist https://github.com/elastic/kibana/issues/62343 const logger = appContextService.getLogger(); return Promise.all( - Object.keys(IndexPatternType).map(async (indexPattern) => { + Object.keys(DataType).map(async (indexPattern) => { const defaultIndexPatternName = indexPattern + INDEX_PATTERN_PLACEHOLDER_SUFFIX; const indexExists = await callCluster('indices.exists', { index: defaultIndexPatternName }); if (!indexExists) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts new file mode 100644 index 0000000000000..5d3e8e9ce87d1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsClientContract, LegacyScopedClusterClient } from 'src/core/server'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { appContextService } from '../../app_context'; +import { createAppContextStartContractMock } from '../../../mocks'; + +jest.mock('../elasticsearch/template/template'); +jest.mock('../kibana/assets/install'); +jest.mock('../kibana/index_pattern/install'); +jest.mock('./install'); +jest.mock('./get'); + +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { installKibanaAssets } from '../kibana/assets/install'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { _installPackage } from './_install_package'; + +const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< + typeof updateCurrentWriteIndices +>; +const mockedGetKibanaAssets = installKibanaAssets as jest.MockedFunction< + typeof installKibanaAssets +>; +const mockedInstallIndexPatterns = installIndexPatterns as jest.MockedFunction< + typeof installIndexPatterns +>; + +function sleep(millis: number) { + return new Promise((resolve) => setTimeout(resolve, millis)); +} + +describe('_installPackage', () => { + let soClient: jest.Mocked; + let callCluster: jest.Mocked; + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(async () => { + appContextService.stop(); + }); + it('handles errors from installIndexPatterns or installKibanaAssets', async () => { + // force errors from either/both these functions + mockedGetKibanaAssets.mockImplementation(async () => { + throw new Error('mocked async error A: should be caught'); + }); + mockedInstallIndexPatterns.mockImplementation(async () => { + throw new Error('mocked async error B: should be caught'); + }); + + // pick any function between when those are called and when await Promise.all is defined later + // and force it to take long enough for the errors to occur + // @ts-expect-error about call signature + mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000)); + + const installationPromise = _installPackage({ + savedObjectsClient: soClient, + callCluster, + pkgName: 'abc', + pkgVersion: '1.2.3', + paths: [], + removable: false, + internal: false, + packageInfo: { + name: 'xyz', + version: '4.5.6', + description: 'test', + type: 'x', + categories: ['this', 'that'], + format_version: 'string', + }, + installType: 'install', + installSource: 'registry', + }); + + // if we have a .catch this will fail nicely (test pass) + // otherwise the test will fail with either of the mocked errors + await expect(installationPromise).rejects.toThrow('mocked'); + await expect(installationPromise).rejects.toThrow('should be caught'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts new file mode 100644 index 0000000000000..f570984cc61aa --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { InstallablePackage, InstallSource } from '../../../../common'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { + AssetReference, + Installation, + CallESAsCurrentUser, + ElasticsearchAssetType, + InstallType, +} from '../../../types'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { installTemplates } from '../elasticsearch/template/install'; +import { generateESIndexPatterns } from '../elasticsearch/template/template'; +import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; +import { installILMPolicy } from '../elasticsearch/ilm/install'; +import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { installTransform } from '../elasticsearch/transform/install'; +import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; + +// this is only exported for testing +// use a leading underscore to indicate it's not the supported path +// only the more explicit `installPackage*` functions should be used + +export async function _installPackage({ + savedObjectsClient, + callCluster, + pkgName, + pkgVersion, + installedPkg, + paths, + removable, + internal, + packageInfo, + installType, + installSource, +}: { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + pkgName: string; + pkgVersion: string; + installedPkg?: SavedObject; + paths: string[]; + removable: boolean; + internal: boolean; + packageInfo: InstallablePackage; + installType: InstallType; + installSource: InstallSource; +}): Promise { + const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams); + // add the package installation to the saved object. + // if some installation already exists, just update install info + if (!installedPkg) { + await createInstallation({ + savedObjectsClient, + pkgName, + pkgVersion, + internal, + removable, + installed_kibana: [], + installed_es: [], + toSaveESIndexPatterns, + installSource, + }); + } else { + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), + install_source: installSource, + }); + } + + // kick off `installIndexPatterns` & `installKibanaAssets` as early as possible because they're the longest running operations + // we don't `await` here because we don't want to delay starting the many other `install*` functions + // however, without an `await` or a `.catch` we haven't defined how to handle a promise rejection + // we define it many lines and potentially seconds of wall clock time later in + // `await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);` + // if we encounter an error before we there, we'll have an "unhandled rejection" which causes its own problems + // the program will log something like this _and exit/crash_ + // Unhandled Promise rejection detected: + // RegistryResponseError or some other error + // Terminating process... + // server crashed with status code 1 + // + // add a `.catch` to prevent the "unhandled rejection" case + // in that `.catch`, set something that indicates a failure + // check for that failure later and act accordingly (throw, ignore, return) + let installIndexPatternError; + const installIndexPatternPromise = installIndexPatterns( + savedObjectsClient, + pkgName, + pkgVersion + ).catch((reason) => (installIndexPatternError = reason)); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); + let installKibanaAssetsError; + const installKibanaAssetsPromise = installKibanaAssets({ + savedObjectsClient, + pkgName, + kibanaAssets, + }).catch((reason) => (installKibanaAssetsError = reason)); + + // the rest of the installation must happen in sequential order + // currently only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per data stream and we should then save them + await installILMPolicy(paths, callCluster); + + // installs versionized pipelines without removing currently installed ones + const installedPipelines = await installPipelines( + packageInfo, + paths, + callCluster, + savedObjectsClient + ); + // install or update the templates referencing the newly installed pipelines + const installedTemplates = await installTemplates( + packageInfo, + callCluster, + paths, + savedObjectsClient + ); + + // update current backing indices of each data stream + await updateCurrentWriteIndices(callCluster, installedTemplates); + + const installedTransforms = await installTransform( + packageInfo, + paths, + callCluster, + savedObjectsClient + ); + + // if this is an update or retrying an update, delete the previous version's pipelines + if ((installType === 'update' || installType === 'reupdate') && installedPkg) { + await deletePreviousPipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.version + ); + } + // pipelines from a different version may have installed during a failed update + if (installType === 'rollback' && installedPkg) { + await deletePreviousPipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.install_version + ); + } + const installedTemplateRefs = installedTemplates.map((template) => ({ + id: template.templateName, + type: ElasticsearchAssetType.indexTemplate, + })); + + // make sure the assets are installed (or didn't error) + if (installIndexPatternError) throw installIndexPatternError; + if (installKibanaAssetsError) throw installKibanaAssetsError; + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + + // update to newly installed version when all assets are successfully installed + if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installed', + }); + return [ + ...installedKibanaAssetsRefs, + ...installedPipelines, + ...installedTemplateRefs, + ...installedTransforms, + ]; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index a7514d1075d78..9651eafbf1e1c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; import Boom from 'boom'; import { UnwrapPromise } from '@kbn/utility-types'; -import { BulkInstallPackageInfo, InstallablePackage, InstallSource } from '../../../../common'; +import { BulkInstallPackageInfo, InstallSource } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -18,10 +18,8 @@ import { AssetType, KibanaAssetReference, EsAssetReference, - ElasticsearchAssetType, InstallType, } from '../../../types'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getInstallation, @@ -30,27 +28,17 @@ import { bulkInstallPackages, isBulkInstallError, } from './index'; -import { installTemplates } from '../elasticsearch/template/install'; -import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; -import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { - installKibanaAssets, - getKibanaAssets, - toAssetReference, - ArchiveAsset, -} from '../kibana/assets/install'; -import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; +import { toAssetReference, ArchiveAsset } from '../kibana/assets/install'; +import { removeInstallation } from './remove'; import { IngestManagerError, PackageOperationNotSupportedError, PackageOutdatedError, } from '../../../errors'; import { getPackageSavedObjects } from './get'; -import { installTransform } from '../elasticsearch/transform/install'; import { appContextService } from '../../app_context'; import { loadArchivePackage } from '../archive'; +import { _installPackage } from './_install_package'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -266,7 +254,7 @@ export async function installPackageFromRegistry({ const { internal = false } = registryPackageInfo; const installSource = 'registry'; - return installPackage({ + return _installPackage({ savedObjectsClient, callCluster, pkgName, @@ -308,7 +296,7 @@ export async function installPackageByUpload({ const { internal = false } = archivePackageInfo; const installSource = 'upload'; - return installPackage({ + return _installPackage({ savedObjectsClient, callCluster, pkgName: archivePackageInfo.name, @@ -323,145 +311,7 @@ export async function installPackageByUpload({ }); } -async function installPackage({ - savedObjectsClient, - callCluster, - pkgName, - pkgVersion, - installedPkg, - paths, - removable, - internal, - packageInfo, - installType, - installSource, -}: { - savedObjectsClient: SavedObjectsClientContract; - callCluster: CallESAsCurrentUser; - pkgName: string; - pkgVersion: string; - installedPkg?: SavedObject; - paths: string[]; - removable: boolean; - internal: boolean; - packageInfo: InstallablePackage; - installType: InstallType; - installSource: InstallSource; -}): Promise { - const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams); - - // add the package installation to the saved object. - // if some installation already exists, just update install info - if (!installedPkg) { - await createInstallation({ - savedObjectsClient, - pkgName, - pkgVersion, - internal, - removable, - installed_kibana: [], - installed_es: [], - toSaveESIndexPatterns, - installSource, - }); - } else { - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installing', - install_started_at: new Date().toISOString(), - install_source: installSource, - }); - } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); - const kibanaAssets = await getKibanaAssets(paths); - if (installedPkg) - await deleteKibanaSavedObjectsAssets( - savedObjectsClient, - installedPkg.attributes.installed_kibana - ); - // save new kibana refs before installing the assets - const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( - savedObjectsClient, - pkgName, - kibanaAssets - ); - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, - pkgName, - kibanaAssets, - }); - - // the rest of the installation must happen in sequential order - - // currently only the base package has an ILM policy - // at some point ILM policies can be installed/modified - // per data stream and we should then save them - await installILMPolicy(paths, callCluster); - - // installs versionized pipelines without removing currently installed ones - const installedPipelines = await installPipelines( - packageInfo, - paths, - callCluster, - savedObjectsClient - ); - // install or update the templates referencing the newly installed pipelines - const installedTemplates = await installTemplates( - packageInfo, - callCluster, - paths, - savedObjectsClient - ); - - // update current backing indices of each data stream - await updateCurrentWriteIndices(callCluster, installedTemplates); - - const installedTransforms = await installTransform( - packageInfo, - paths, - callCluster, - savedObjectsClient - ); - - // if this is an update or retrying an update, delete the previous version's pipelines - if ((installType === 'update' || installType === 'reupdate') && installedPkg) { - await deletePreviousPipelines( - callCluster, - savedObjectsClient, - pkgName, - installedPkg.attributes.version - ); - } - // pipelines from a different version may have installed during a failed update - if (installType === 'rollback' && installedPkg) { - await deletePreviousPipelines( - callCluster, - savedObjectsClient, - pkgName, - installedPkg.attributes.install_version - ); - } - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); - - // update to newly installed version when all assets are successfully installed - if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installed', - }); - return [ - ...installedKibanaAssetsRefs, - ...installedPipelines, - ...installedTemplateRefs, - ...installedTransforms, - ]; -} - -const updateVersion = async ( +export const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, pkgVersion: string diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 417f2871a6cbf..8d5995f92c95f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -44,9 +44,6 @@ export async function removeInstallation(options: { `unable to remove package with existing package policy(s) in use by agent(s)` ); - // recreate or delete index patterns when a package is uninstalled - await installIndexPatterns(savedObjectsClient); - // Delete the installed assets const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; await deleteAssets(installedAssets, savedObjectsClient, callCluster); @@ -55,6 +52,11 @@ export async function removeInstallation(options: { // could also update with [] or some other state await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); + // recreate or delete index patterns when a package is uninstalled + // this must be done after deleting the saved object for the current package otherwise it will retrieve the package + // from the registry again and reinstall the index patterns + await installIndexPatterns(savedObjectsClient); + // remove the package archive and its contents from the cache so that a reinstall fetches // a fresh copy from the registry deletePackageCache(pkgName, pkgVersion); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 0c070959e3b93..7d841ed024ce5 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -73,6 +73,7 @@ export { // Agent Request types PostAgentEnrollRequest, PostAgentCheckinRequest, + DataType, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index af44fc28fec15..2bbf183b7ae11 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -14,6 +14,7 @@ import { CoreStart, CoreSetup } from 'kibana/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; import { ExpressionRendererEvent, + ExpressionRenderError, ReactExpressionRendererType, } from '../../../../../../../src/plugins/expressions/public'; import { Action } from '../state_management'; @@ -40,6 +41,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { DropIllustration } from '../../../assets/drop_illustration'; +import { getOriginalRequestErrorMessage } from '../../error_helper'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -342,7 +344,8 @@ export const InnerVisualizationWrapper = ({ searchContext={context} reload$={autoRefreshFetch$} onEvent={onEvent} - renderError={(errorMessage?: string | null) => { + renderError={(errorMessage?: string | null, error?: ExpressionRenderError | null) => { + const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; return ( @@ -354,7 +357,7 @@ export const InnerVisualizationWrapper = ({ defaultMessage="An error occurred when loading data." /> - {errorMessage ? ( + {visibleErrorMessage ? ( { @@ -369,7 +372,7 @@ export const InnerVisualizationWrapper = ({ })} - {localState.expandError ? errorMessage : null} + {localState.expandError ? visibleErrorMessage : null} ) : null} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index d0d2360ddc107..4fb0630a305e7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -13,6 +13,7 @@ import { ReactExpressionRendererType, } from 'src/plugins/expressions/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; +import { getOriginalRequestErrorMessage } from '../error_helper'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -50,7 +51,20 @@ export function ExpressionWrapper({ padding="m" expression={expression} searchContext={searchContext} - renderError={(error) =>
{error}
} + renderError={(errorMessage, error) => ( +
+ + + + + + + {getOriginalRequestErrorMessage(error) || errorMessage} + + + +
+ )} onEvent={handleEvent} />
diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts new file mode 100644 index 0000000000000..79faa5a47def3 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ExpressionRenderError } from 'src/plugins/expressions/public'; + +interface ElasticsearchErrorClause { + type: string; + reason: string; + caused_by?: ElasticsearchErrorClause; +} + +interface RequestError extends Error { + body?: { attributes?: { error: ElasticsearchErrorClause } }; +} + +const isRequestError = (e: Error | RequestError): e is RequestError => { + if ('body' in e) { + return e.body?.attributes?.error?.caused_by !== undefined; + } + return false; +}; + +function getNestedErrorClause({ + type, + reason, + caused_by: causedBy, +}: ElasticsearchErrorClause): { type: string; reason: string } { + if (causedBy) { + return getNestedErrorClause(causedBy); + } + return { type, reason }; +} + +export function getOriginalRequestErrorMessage(error?: ExpressionRenderError | null) { + if (error && 'original' in error && error.original && isRequestError(error.original)) { + const rootError = getNestedErrorClause(error.original.body!.attributes!.error); + if (rootError.reason && rootError.type) { + return i18n.translate('xpack.lens.editorFrame.expressionFailureMessage', { + defaultMessage: 'Request error: {type}, {reason}', + values: { + reason: rootError.reason, + type: rootError.type, + }, + }); + } + } +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 16b861ae034fa..96f4120e3df78 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -25,7 +25,12 @@ import { keys } from '@elastic/eui'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; import { RangeTypeLens, isValidRange, isValidNumber } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; -import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; +import { + NewBucketButton, + DragDropBuckets, + DraggableBucketContainer, + LabelInput, +} from '../shared_components'; const generateId = htmlIdGenerator(); @@ -63,7 +68,7 @@ export const RangePopover = ({ // send the range back to the main state setRange(newRange); }; - const { from, to } = tempRange; + const { from, to, label } = tempRange; const lteAppendLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanOrEqualAppend', { defaultMessage: '\u2264', @@ -159,6 +164,25 @@ export const RangePopover = ({ + + { + const newRange = { + ...tempRange, + label: newLabel, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + placeholder={i18n.translate( + 'xpack.lens.indexPattern.ranges.customRangeLabelPlaceholder', + { defaultMessage: 'Custom label' } + )} + onSubmit={onSubmit} + dataTestSubj="indexPattern-ranges-label" + /> + ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index fb6cf6df8573f..5317ee913fcdd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -23,6 +23,7 @@ import { } from './constants'; import { RangePopover } from './advanced_editor'; import { DragDropBuckets } from '../shared_components'; +import { EuiFieldText } from '@elastic/eui'; const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first @@ -152,6 +153,25 @@ describe('ranges', () => { }) ); }); + + it('should include custom labels', () => { + setToRangeMode(); + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges = [ + { from: 0, to: 100, label: 'customlabel' }, + ]; + + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + + expect((esAggsConfig as { params: unknown }).params).toEqual( + expect.objectContaining({ + ranges: [{ from: 0, to: 100, label: 'customlabel' }], + }) + ); + }); }); describe('getPossibleOperationForField', () => { @@ -419,6 +439,63 @@ describe('ranges', () => { }); }); + it('should add a new range with custom label', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // edit the label and check + instance.find(RangePopover).find(EuiFieldText).first().prop('onChange')!({ + target: { + value: 'customlabel', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [ + { from: 0, to: DEFAULT_INTERVAL, label: '' }, + { from: DEFAULT_INTERVAL, to: Infinity, label: 'customlabel' }, + ], + }, + }, + }, + }, + }, + }); + }); + }); + it('should open a popover to edit an existing range', () => { const setStateSpy = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index a8304456262eb..a256f5e4ecfa1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -61,9 +61,9 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { field: sourceField, ranges: params.ranges.filter(isValidRange).map>((range) => { if (isFullRange(range)) { - return { from: range.from, to: range.to }; + return range; } - const partialRange: Partial = {}; + const partialRange: Partial = { label: range.label }; // be careful with the fields to set on partial ranges if (isValidNumber(range.from)) { partialRange.from = range.from; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 09a2cc652a9b3..1ab00eef0593b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -518,6 +518,22 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeTruthy(); }); + test('respects requested sub visualization type if set', () => { + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'reduced', + }, + keptLayerIds: [], + subVisualizationId: 'area', + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state.preferredSeriesType).toBe('area'); + }); + test('keeps existing seriesType for initial tables', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index e6286523d8e2e..9e1d42a0f58c6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -35,6 +35,7 @@ export function getSuggestions({ table, state, keptLayerIds, + subVisualizationId, }: SuggestionRequest): Array> { if ( // We only render line charts for multi-row queries. We require at least @@ -66,7 +67,12 @@ export function getSuggestions({ return []; } - const suggestions = getSuggestionForColumns(table, keptLayerIds, state); + const suggestions = getSuggestionForColumns( + table, + keptLayerIds, + state, + subVisualizationId as SeriesType | undefined + ); if (suggestions && suggestions instanceof Array) { return suggestions; @@ -78,7 +84,8 @@ export function getSuggestions({ function getSuggestionForColumns( table: TableSuggestion, keptLayerIds: string[], - currentState?: State + currentState?: State, + seriesType?: SeriesType ): VisualizationSuggestion | Array> | undefined { const [buckets, values] = partition(table.columns, (col) => col.operation.isBucketed); @@ -93,6 +100,7 @@ function getSuggestionForColumns( currentState, tableLabel: table.label, keptLayerIds, + requestedSeriesType: seriesType, }); } else if (buckets.length === 0) { const [x, ...yValues] = prioritizeColumns(values); @@ -105,6 +113,7 @@ function getSuggestionForColumns( currentState, tableLabel: table.label, keptLayerIds, + requestedSeriesType: seriesType, }); } } @@ -190,6 +199,7 @@ function getSuggestionsForLayer({ currentState, tableLabel, keptLayerIds, + requestedSeriesType, }: { layerId: string; changeType: TableChangeType; @@ -199,9 +209,11 @@ function getSuggestionsForLayer({ currentState?: State; tableLabel?: string; keptLayerIds: string[]; + requestedSeriesType?: SeriesType; }): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); - const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue); + const seriesType: SeriesType = + requestedSeriesType || getSeriesType(currentState, layerId, xValue); const options = { currentState, diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index ce3fcdb042326..f82e0488d0386 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -240,6 +240,46 @@ describe('licensing plugin', () => { expect(coreSetup.http.get).toHaveBeenCalledTimes(1); }); + it('http interceptor does not trigger re-fetch if signature header is not present', async () => { + const sessionStorage = coreMock.createStorage(); + plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); + + const coreSetup = coreMock.createSetup(); + + coreSetup.http.get.mockResolvedValue(licenseMock.createLicense({ signature: 'signature-1' })); + + let registeredInterceptor: HttpInterceptor; + coreSetup.http.intercept.mockImplementation((interceptor: HttpInterceptor) => { + registeredInterceptor = interceptor; + return () => undefined; + }); + + await plugin.setup(coreSetup); + await plugin.start(coreStart); + expect(registeredInterceptor!.response).toBeDefined(); + + const httpResponse = { + response: { + headers: { + get(name: string) { + if (name === 'kbn-license-sig') { + return undefined; + } + throw new Error('unexpected header'); + }, + }, + }, + request: { + url: 'http://10.10.10.10:5601/api/hello', + }, + }; + expect(coreSetup.http.get).toHaveBeenCalledTimes(0); + + await registeredInterceptor!.response!(httpResponse as any, null as any); + + expect(coreSetup.http.get).toHaveBeenCalledTimes(0); + }); + it('http interceptor does not trigger license re-fetch for anonymous pages', async () => { const sessionStorage = coreMock.createStorage(); plugin = new LicensingPlugin(coreMock.createPluginInitializerContext(), sessionStorage); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 3f945756691b3..f5832d0bb7d93 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -101,7 +101,7 @@ export class LicensingPlugin implements Plugin coreStart.application.capabilities export const getDocLinks = () => coreStart.docLinks; export const getCoreOverlays = () => coreStart.overlays; export const getData = () => pluginsStart.data; +export const getSavedObjects = () => pluginsStart.savedObjects; export const getUiActions = () => pluginsStart.uiActions; export const getCore = () => coreStart; export const getNavigation = () => pluginsStart.navigation; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index a2b629bdd4989..0b797c7b8ef60 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -50,6 +50,7 @@ import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { StartContract as FileUploadStartContract } from '../../file_upload/public'; +import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { registerLicensedFeatures, setLicensingPluginStart } from './licensed_features'; export interface MapsPluginSetupDependencies { @@ -71,6 +72,7 @@ export interface MapsPluginStartDependencies { navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; share: SharePluginStart; + savedObjects: SavedObjectsStart; } /** diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts index 66af92c7a687b..fe8aa02615b85 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts @@ -7,23 +7,10 @@ import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; -import { - getCoreChrome, - getSavedObjectsClient, - getIndexPatternService, - getCoreOverlays, - getData, -} from '../../../kibana_services'; +import { getSavedObjects, getSavedObjectsClient } from '../../../kibana_services'; export const getMapsSavedObjectLoader = _.once(function () { - const services = { - savedObjectsClient: getSavedObjectsClient(), - indexPatterns: getIndexPatternService(), - search: getData().search, - chrome: getCoreChrome(), - overlays: getCoreOverlays(), - }; - const SavedGisMap = createSavedGisMapClass(services); + const SavedGisMap = createSavedGisMapClass(getSavedObjects()); return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient()); }); diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts index 511f015b0ff80..7b31d9edea90d 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts @@ -8,9 +8,8 @@ import _ from 'lodash'; import { SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { - createSavedObjectClass, + SavedObjectsStart, SavedObject, - SavedObjectKibanaServices, } from '../../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, @@ -40,10 +39,8 @@ export interface ISavedGisMap extends SavedObject { syncWithStore(): void; } -export function createSavedGisMapClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedGisMap extends SavedObjectClass implements ISavedGisMap { +export function createSavedGisMapClass(savedObjects: SavedObjectsStart) { + class SavedGisMap extends savedObjects.SavedObjectClass implements ISavedGisMap { public static type = MAP_SAVED_OBJECT_TYPE; // Mappings are used to place object properties into saved object _source diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index a33b2e6b3e2d6..f88694a1952b2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -208,14 +208,24 @@ export const useRenderCellValue = ( return results[cId.replace(`${resultsField}.`, '')]; } - return tableItems.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) - : null; + if (tableItems.hasOwnProperty(adjustedRowIndex)) { + const item = tableItems[adjustedRowIndex]; + + // Try if the field name is available as is. + if (item.hasOwnProperty(cId)) { + return item[cId]; + } + + // Try if the field name is available as a nested field. + return getNestedProperty(tableItems[adjustedRowIndex], cId, null); + } + + return null; } const cellValue = getCellValue(columnId); - // React by default doesn't all us to use a hook in a callback. + // React by default doesn't allow us to use a hook in a callback. // However, this one will be passed on to EuiDataGrid and its docs // recommend wrapping `setCellProps` in a `useEffect()` hook // so we're ignoring the linting rule here. diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss new file mode 100644 index 0000000000000..2e2f4e7af0a25 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss @@ -0,0 +1,6 @@ +.mlDataGrid { + .euiDataGridRowCell--boolean { + text-transform: none; + } +} + diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index f613c5fdb3450..fad2439f5d5ee 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -34,6 +34,7 @@ import { TopClasses } from '../../../../common/types/feature_importance'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; +import './data_grid.scss'; // TODO Fix row hovering + bar highlighting // import { hoveredRow$ } from './column_chart'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index a00284860d668..d25d2c3a858ed 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -109,24 +109,22 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J showFlyout(); } - const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({ - newSelection, - jobIds, - groups: newGroups, - time, - }) => { - setSelectedIds(newSelection); - - setGlobalState({ - ml: { - jobIds, - groups: newGroups, - }, - ...(time !== undefined ? { time } : {}), - }); - - closeFlyout(); - }; + const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback( + ({ newSelection, jobIds, groups: newGroups, time }) => { + setSelectedIds(newSelection); + + setGlobalState({ + ml: { + jobIds, + groups: newGroups, + }, + ...(time !== undefined ? { time } : {}), + }); + + closeFlyout(); + }, + [setGlobalState, setSelectedIds] + ); function renderJobSelectionBar() { return ( diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx index 1e8ac4c15fd15..a90eb8cfde532 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -82,7 +82,7 @@ export const JobSelectorFlyoutContent: FC = ({ const flyoutEl = useRef(null); - function applySelection() { + const applySelection = useCallback(() => { // allNewSelection will be a list of all job ids (including those from groups) selected from the table const allNewSelection: string[] = []; const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; @@ -110,7 +110,7 @@ export const JobSelectorFlyoutContent: FC = ({ groups: groupSelection, time, }); - } + }, [onSelectionConfirmed, newSelection, jobGroupsMaps, applyTimeRange]); function removeId(id: string) { setNewSelection(newSelection.filter((item) => item !== id)); diff --git a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 0717348d1db22..04fa3e9201c6c 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -88,7 +88,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[]) => { ...(time !== undefined ? { time } : {}), }); } - }, [jobs, validIds]); + }, [jobs, validIds, setGlobalState, globalState?.ml]); return jobSelection; }; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index beafae1ecd2f6..409bd11e0bde3 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -54,7 +54,7 @@ export const DatePickerWrapper: FC = () => { useEffect(() => { setGlobalState({ refreshInterval }); timefilter.setRefreshInterval(refreshInterval); - }, [refreshInterval?.pause, refreshInterval?.value]); + }, [refreshInterval?.pause, refreshInterval?.value, setGlobalState]); const [time, setTime] = useState(timefilter.getTime()); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges()); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss index 00463affa0d03..f1365db31eca7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -32,4 +32,8 @@ .mlDataFrameAnalyticsClassification__dataGridMinWidth { min-width: 480px; width: 100%; + + .euiDataGridRowCell--boolean { + text-transform: none; + } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 3746fa12bdc1e..d1889a8acb990 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -17,7 +17,14 @@ export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] return 0; } - return Object.keys(tableItems[0]).filter((key) => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; + const fullItem = tableItems[0]; + + if ( + fullItem[resultsField] !== undefined && + Array.isArray(fullItem[resultsField][FEATURE_INFLUENCE]) + ) { + return fullItem[resultsField][FEATURE_INFLUENCE].length; + } + + return 0; }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index bf6b48fa18b47..12e95e859af53 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -15,6 +15,7 @@ import { get, each, find, sortBy, map, reduce } from 'lodash'; import { buildConfig } from './explorer_chart_config_builder'; import { chartLimits, getChartType } from '../../util/chart_utils'; +import { getTimefilter } from '../../util/dependency_cache'; import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; import { @@ -50,8 +51,8 @@ const MAX_CHARTS_PER_ROW = 4; export const anomalyDataChange = function ( chartsContainerWidth, anomalyRecords, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, severity = 0 ) { const data = getDefaultChartsData(); @@ -83,8 +84,8 @@ export const anomalyDataChange = function ( const chartWidth = Math.floor(chartsContainerWidth / chartsPerRow); const { chartRange, tooManyBuckets } = calculateChartRange( seriesConfigs, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, chartWidth, recordsToPlot, data.timeFieldName @@ -408,8 +409,8 @@ export const anomalyDataChange = function ( chartData: processedData[i], plotEarliest: chartRange.min, plotLatest: chartRange.max, - selectedEarliest: earliestMs, - selectedLatest: latestMs, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, chartLimits: USE_OVERALL_CHART_LIMITS ? overallChartLimits : chartLimits(processedData[i]), })); explorerService.setCharts({ ...data }); @@ -561,8 +562,8 @@ function processRecordsForDisplay(anomalyRecords) { function calculateChartRange( seriesConfigs, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, chartWidth, recordsToPlot, timeFieldName @@ -570,10 +571,12 @@ function calculateChartRange( let tooManyBuckets = false; // Calculate the time range for the charts. // Fit in as many points in the available container width plotted at the job bucket span. - const midpointMs = Math.ceil((earliestMs + latestMs) / 2); + const midpointMs = Math.ceil((selectedEarliestMs + selectedLatestMs) / 2); const maxBucketSpanMs = Math.max.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000; - const pointsToPlotFullSelection = Math.ceil((latestMs - earliestMs) / maxBucketSpanMs); + const pointsToPlotFullSelection = Math.ceil( + (selectedLatestMs - selectedEarliestMs) / maxBucketSpanMs + ); // Optimally space points 5px apart. const optimumPointSpacing = 5; @@ -583,9 +586,12 @@ function calculateChartRange( // at optimal point spacing. const plotPoints = Math.max(optimumNumPoints, pointsToPlotFullSelection); const halfPoints = Math.ceil(plotPoints / 2); + const timefilter = getTimefilter(); + const bounds = timefilter.getActiveBounds(); + let chartRange = { - min: midpointMs - halfPoints * maxBucketSpanMs, - max: midpointMs + halfPoints * maxBucketSpanMs, + min: Math.max(midpointMs - halfPoints * maxBucketSpanMs, bounds.min.valueOf()), + max: Math.min(midpointMs + halfPoints * maxBucketSpanMs, bounds.max.valueOf()), }; if (plotPoints > CHART_MAX_POINTS) { @@ -615,8 +621,8 @@ function calculateChartRange( if (maxMs - minMs < maxTimeSpan) { // Expand out to cover as much as the requested time span as possible. - minMs = Math.max(earliestMs, minMs - maxTimeSpan); - maxMs = Math.min(latestMs, maxMs + maxTimeSpan); + minMs = Math.max(selectedEarliestMs, minMs - maxTimeSpan); + maxMs = Math.min(selectedLatestMs, maxMs + maxTimeSpan); } chartRange = { min: minMs, max: maxMs }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index 5e6901408422b..8678e99114131 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -15,7 +15,7 @@ import mockSeriesPromisesResponse from './__mocks__/mock_series_promises_respons // // 'call anomalyChangeListener with actual series config' // This test uses the standard mocks and uses the data as is provided via the mock files. -// The mocked services check for values in the data (e.g. 'mock-job-id', 'farequore-2017') +// The mocked services check for values in the data (e.g. 'mock-job-id', 'farequote-2017') // and return the mock data from the files. // // 'filtering should skip values of null' @@ -88,14 +88,41 @@ jest.mock('../../util/string_utils', () => ({ }, })); +jest.mock('../../util/dependency_cache', () => { + const dateMath = require('@elastic/datemath'); + let _time = undefined; + const timefilter = { + setTime: (time) => { + _time = time; + }, + getActiveBounds: () => { + return { + min: dateMath.parse(_time.from), + max: dateMath.parse(_time.to), + }; + }, + }; + return { + getTimefilter: () => timefilter, + }; +}); + jest.mock('../explorer_dashboard_service', () => ({ explorerService: { setCharts: jest.fn(), }, })); +import moment from 'moment'; import { anomalyDataChange, getDefaultChartsData } from './explorer_charts_container_service'; import { explorerService } from '../explorer_dashboard_service'; +import { getTimefilter } from '../../util/dependency_cache'; + +const timefilter = getTimefilter(); +timefilter.setTime({ + from: moment(1486425600000).toISOString(), // Feb 07 2017 + to: moment(1486857600000).toISOString(), // Feb 12 2017 +}); describe('explorerChartsContainerService', () => { afterEach(() => { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index c309e1f4ef8e8..c3bdacde5abd8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -198,7 +198,7 @@ export function getSelectionTimeRange(selectedCells, interval, bounds) { latestMs = bounds.max.valueOf(); if (selectedCells.times[1] !== undefined) { // Subtract 1 ms so search does not include start of next bucket. - latestMs = (selectedCells.times[1] + interval) * 1000 - 1; + latestMs = selectedCells.times[1] * 1000 - 1; } } diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index f356d79c0a8e1..c7cda2372bceb 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -60,7 +60,7 @@ export const useSelectedCells = ( setAppState('mlExplorerSwimlane', mlExplorerSwimlane); } }, - [appState?.mlExplorerSwimlane, selectedCells] + [appState?.mlExplorerSwimlane, selectedCells, setAppState] ); return [selectedCells, setSelectedCells]; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 0a2791edb9c50..9d6d6c14ed659 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -41,6 +41,7 @@ import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; +import { useUiSettings } from '../contexts/kibana'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -159,6 +160,8 @@ export const SwimlaneContainer: FC = ({ }) => { const [chartWidth, setChartWidth] = useState(0); + const isDarkTheme = !!useUiSettings().get('theme:darkMode'); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); @@ -235,67 +238,76 @@ export const SwimlaneContainer: FC = ({ return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; }, [selection, swimlaneData, swimlaneType]); - const swimLaneConfig: HeatmapSpec['config'] = useMemo( - () => - showSwimlane - ? { - onBrushEnd: (e: HeatmapBrushEvent) => { - onCellsSelection({ - lanes: e.y as string[], - times: e.x.map((v) => (v as number) / 1000), - type: swimlaneType, - viewByFieldName: swimlaneData.fieldName, - }); - }, - grid: { - cellHeight: { - min: CELL_HEIGHT, - max: CELL_HEIGHT, - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - formatter: (laneLabel: string) => { - return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; - }, - }, - xAxisLabel: { - visible: showTimeline, - // eui color subdued - fill: `#98A2B3`, - formatter: (v: number) => { - timeBuckets.setInterval(`${swimlaneData.interval}s`); - const a = timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - brushMask: { - fill: 'rgb(247 247 247 / 50%)', - }, - maxLegendHeight: LEGEND_HEIGHT, - } - : {}, - [showSwimlane, swimlaneType, swimlaneData?.fieldName] - ); + const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => { + if (!showSwimlane) return {}; + + return { + onBrushEnd: (e: HeatmapBrushEvent) => { + onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT, + max: CELL_HEIGHT, + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + formatter: (laneLabel: string) => { + return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; + }, + }, + xAxisLabel: { + visible: showTimeline, + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + timeBuckets.setInterval(`${swimlaneData.interval}s`); + const scaledDateFormat = timeBuckets.getScaledDateFormat(); + return moment(v).format(scaledDateFormat); + }, + }, + brushMask: { + fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)', + }, + brushArea: { + stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)', + }, + maxLegendHeight: LEGEND_HEIGHT, + timeZone: 'UTC', + }; + }, [ + showSwimlane, + swimlaneType, + swimlaneData?.fieldName, + isDarkTheme, + timeBuckets, + onCellsSelection, + ]); // @ts-ignore const onElementClick: ElementClickListener = useCallback( @@ -310,7 +322,7 @@ export const SwimlaneContainer: FC = ({ }; onCellsSelection(payload); }, - [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] + [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval, onCellsSelection] ); const tooltipOptions: TooltipSettings = useMemo( diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 00d64a2f1bd1d..cb6944e0ecf05 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -82,8 +82,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const { jobIds } = useJobSelection(jobsWithTimeRange); const refresh = useRefresh(); + useEffect(() => { - if (refresh !== undefined) { + if (refresh !== undefined && lastRefresh !== refresh.lastRefresh) { setLastRefresh(refresh?.lastRefresh); if (refresh.timeRange !== undefined) { @@ -94,7 +95,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim }); } } - }, [refresh?.lastRefresh]); + }, [refresh?.lastRefresh, lastRefresh, setLastRefresh, setGlobalState]); // We cannot simply infer bounds from the globalState's `time` attribute // with `moment` since it can contain custom strings such as `now-15m`. @@ -194,6 +195,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableSeverity] = useTableSeverity(); const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState); + useEffect(() => { explorerService.setSelectedCells(selectedCells); }, [JSON.stringify(selectedCells)]); @@ -220,9 +222,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (explorerState && explorerState.swimlaneContainerWidth > 0) { loadExplorerData({ ...loadExplorerDataConfig, - swimlaneLimit: - isViewBySwimLaneData(explorerState?.viewBySwimlaneData) && - explorerState?.viewBySwimlaneData.cardinality, + swimlaneLimit: isViewBySwimLaneData(explorerState?.viewBySwimlaneData) + ? explorerState?.viewBySwimlaneData.cardinality + : undefined, }); } }, [JSON.stringify(loadExplorerDataConfig)]); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index 50cacd7b3545a..0d4bba26f1c0e 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -33,7 +33,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` }, ] } - data-test-subj="mlCalendarTable" + data-test-subj="mlCalendarTable loaded" isSelectable={true} itemId="calendar_id" items={ diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index 6b4403aef7c7b..d59639fd44ea2 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -142,7 +142,7 @@ export const CalendarsListTable = ({ loading={loading} selection={tableSelection} isSelectable={true} - data-test-subj="mlCalendarTable" + data-test-subj={loading ? 'mlCalendarTable loading' : 'mlCalendarTable loaded'} rowProps={(item) => ({ 'data-test-subj': `mlCalendarListRow row-${item.calendar_id}`, })} diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index c288a00bb06da..a3c70e1130904 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -140,12 +140,12 @@ export const useUrlState = (accessor: Accessor) => { if (typeof fullUrlState === 'object') { return fullUrlState[accessor]; } - return undefined; }, [searchString]); const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any) => - setUrlStateContext(accessor, attribute, value), + (attribute: string | Dictionary, value?: any) => { + setUrlStateContext(accessor, attribute, value); + }, [accessor, setUrlStateContext] ); return [urlState, setUrlState]; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 6e67ff1aef03d..4730371c611c1 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -59,17 +60,19 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ReactDOM.render( - - - + + + + + , node ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 17ae97e3c07bb..5efe70ba552f5 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -89,7 +89,7 @@ export const EmbeddableSwimLaneContainer: FC = ( }); } }, - [swimlaneData, perPage, fromPage] + [swimlaneData, perPage, fromPage, setSelectedCells] ); if (error) { diff --git a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx index 325e903de0e2d..79e6ff53bff43 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx @@ -39,8 +39,7 @@ export function createApplyTimeRangeSelectionAction( let [from, to] = data.times; from = from * 1000; - // extend bounds with the interval - to = to * 1000 + interval * 1000; + to = to * 1000; timefilter.setTime({ from: moment(from), diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index c7a1c79748b5b..abd86d51fb6b6 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -195,21 +195,21 @@ export class ReportingCore { return scopedUiSettingsService; } - public getSpaceId(request: KibanaRequest): string | undefined { + public getSpaceId(request: KibanaRequest, logger = this.logger): string | undefined { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; if (spacesService) { const spaceId = spacesService?.getSpaceId(request); if (spaceId !== DEFAULT_SPACE_ID) { - this.logger.info(`Request uses Space ID: ` + spaceId); + logger.info(`Request uses Space ID: ${spaceId}`); return spaceId; } else { - this.logger.info(`Request uses default Space`); + logger.debug(`Request uses default Space`); } } } - public getFakeRequest(baseRequest: object, spaceId?: string) { + public getFakeRequest(baseRequest: object, spaceId: string | undefined, logger = this.logger) { const fakeRequest = KibanaRequest.from({ path: '/', route: { settings: {} }, @@ -221,7 +221,7 @@ export class ReportingCore { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; if (spacesService) { if (spaceId && spaceId !== DEFAULT_SPACE_ID) { - this.logger.info(`Generating request for space: ` + spaceId); + logger.info(`Generating request for space: ${spaceId}`); this.getPluginSetupDeps().basePath.set(fakeRequest, `/s/${spaceId}`); } } @@ -229,11 +229,11 @@ export class ReportingCore { return fakeRequest; } - public async getUiSettingsClient(request: KibanaRequest) { + public async getUiSettingsClient(request: KibanaRequest, logger = this.logger) { const spacesService = this.getPluginSetupDeps().spaces?.spacesService; - const spaceId = this.getSpaceId(request); + const spaceId = this.getSpaceId(request, logger); if (spacesService && spaceId) { - this.logger.info(`Creating UI Settings Client for space: ${spaceId}`); + logger.info(`Creating UI Settings Client for space: ${spaceId}`); } const savedObjectsClient = await this.getSavedObjectsClient(request); return await this.getUiSettingsServiceFactory(savedObjectsClient); diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index cb60b218818f0..5b98a198b7d1a 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CSV_JOB_TYPE } from '../../../constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; @@ -11,7 +12,9 @@ import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { + const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']); + const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); @@ -26,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); return async function runTask(jobId, job, cancellationToken) { const elasticsearch = reporting.getElasticsearchService(); - const jobLogger = logger.clone([jobId]); - const generateCsv = createGenerateCsv(jobLogger); + const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + const generateCsv = createGenerateCsv(logger); const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); - const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId); - const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest); + const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId, logger); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(fakeRequest); const callEndpoint = (endpoint: string, clientParams = {}, options = {}) => diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index 19348c0a678d7..5e95eec99871f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -32,11 +32,10 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); return async function runTask(jobId, jobPayload, context, req) { - const jobLogger = logger.clone(['immediate']); - const generateCsv = createGenerateCsv(jobLogger); + const generateCsv = createGenerateCsv(logger); const { panel, visType } = jobPayload; - jobLogger.debug(`Execute job generating [${visType}] csv`); + logger.debug(`Execute job generating [${visType}] csv`); const savedObjectsClient = context.core.savedObjects.client; const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); @@ -54,11 +53,11 @@ export const runTaskFnFactory: RunTaskFnFactory = function e ); if (csvContainsFormulas) { - jobLogger.warn(`CSV may contain formulas whose values have been escaped`); + logger.warn(`CSV may contain formulas whose values have been escaped`); } if (maxSizeReached) { - jobLogger.warn(`Max size reached: CSV output truncated to ${size} bytes`); + logger.warn(`Max size reached: CSV output truncated to ${size} bytes`); } return { diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index eaaa11d461156..b1fcdbe05fd67 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PNG_JOB_TYPE } from '../../../../constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; @@ -12,7 +13,8 @@ import { JobParamsPNG, TaskPayloadPNG } from '../types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { + const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute-job']); const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); @@ -27,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([PDF_JOB_TYPE, 'create-job']); return async function createJob( { title, relativeUrls, browserTimezone, layout, objectType }, @@ -27,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory void } | null | undefined; @@ -40,7 +39,9 @@ export const runTaskFnFactory: RunTaskFnFactory decryptJobHeaders(encryptionKey, job.headers, logger)), map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), - mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)), + mergeMap((conditionalHeaders) => + getCustomLogo(reporting, conditionalHeaders, job.spaceId, logger) + ), mergeMap(({ logo, conditionalHeaders }) => { const urls = getFullUrls(config, job); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index 426770d719069..9f7e9310333ba 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -8,6 +8,7 @@ import { ReportingConfig, ReportingCore } from '../../../'; import { createMockConfig, createMockConfigSchema, + createMockLevelLogger, createMockReportingCore, } from '../../../test_helpers'; import { getConditionalHeaders } from '../../common'; @@ -16,6 +17,8 @@ import { getCustomLogo } from './get_custom_logo'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; +const logger = createMockLevelLogger(); + beforeEach(async () => { mockConfig = createMockConfig(createMockConfigSchema()); mockReportingPlugin = await createMockReportingCore(mockConfig); @@ -40,7 +43,12 @@ test(`gets logo from uiSettings`, async () => { const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders); - const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders); + const { logo } = await getCustomLogo( + mockReportingPlugin, + conditionalHeaders, + 'spaceyMcSpaceIdFace', + logger + ); expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo'); expect(logo).toBe('purple pony'); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts index 7bd1637db1379..98185a1acf5e8 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts @@ -6,16 +6,21 @@ import { ReportingCore } from '../../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; import { ConditionalHeaders } from '../../common'; export const getCustomLogo = async ( reporting: ReportingCore, conditionalHeaders: ConditionalHeaders, - spaceId?: string + spaceId: string | undefined, + logger: LevelLogger ) => { - const fakeRequest = reporting.getFakeRequest({ headers: conditionalHeaders.headers }, spaceId); - const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest); - + const fakeRequest = reporting.getFakeRequest( + { headers: conditionalHeaders.headers }, + spaceId, + logger + ); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); const logo: string = await uiSettingsClient.get(UI_SETTINGS_CUSTOM_PDF_LOGO); // continue the pipeline diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index f12b76ccce847..4cecc2e24867f 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -6,7 +6,8 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { getExportTypesRegistry } from '../lib/export_types_registry'; import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; @@ -56,6 +57,11 @@ function getPluginsMock( const getResponseMock = (base = {}) => base; +const getMockFetchClients = (resp: any) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue(resp); + return fetchParamsMock; +}; describe('license checks', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; @@ -68,7 +74,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -78,7 +83,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -98,7 +103,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'none' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -108,7 +112,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -128,7 +132,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'platinum' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -138,7 +141,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -158,7 +161,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve({})); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -168,7 +170,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients({})); }); test('sets enables to true', async () => { @@ -184,6 +186,7 @@ describe('license checks', () => { describe('data modeling', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; + let collectorFetchContext: CollectorFetchContext; beforeAll(async () => { mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); @@ -199,44 +202,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 12, - jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, - layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, - objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, - statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, - statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, - }, - last7Days: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, - lastDay: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, + collectorFetchContext = getMockFetchClients( + getResponseMock( + { + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 12, + jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, + layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, + objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, + statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, + statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, + }, + last7Days: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + }, + lastDay: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, }, }, }, - } as SearchResponse) // prettier-ignore - ) + }, + } as SearchResponse) // prettier-ignore ); - - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); @@ -251,44 +252,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - last7Days: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - lastDay: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, + collectorFetchContext = getMockFetchClients( + getResponseMock( + { + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + last7Days: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + lastDay: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, }, }, }, - } as SearchResponse) // prettier-ignore - ) + }, + } as SearchResponse) // prettier-ignore ); - - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); @@ -303,43 +302,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - last7Days: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - lastDay: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - }, + + collectorFetchContext = getMockFetchClients( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + last7Days: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + lastDay: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, }, }, - } as SearchResponse) - ) + }, + }, + } as SearchResponse) // prettier-ignore ); - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 176d3dcb37dfc..2ef7a7995b839 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,8 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { LegacyAPICaller } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { ReportingSetupDeps } from '../types'; @@ -37,7 +36,7 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: (callCluster: LegacyAPICaller) => { + fetch: ({ callCluster }: CollectorFetchContext) => { const config = reporting.getConfig(); return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); }, diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index daacc065629a4..33bb430aefe5e 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { LegacyAPICaller } from 'kibana/server'; interface IdToFlagMap { @@ -211,7 +211,7 @@ export function registerRollupUsageCollector( total: { type: 'long' }, }, }, - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 87bcc96d1f9d4..700653c4cecb8 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -147,7 +147,7 @@ export class SecurityPlugin public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) { this.sessionTimeout.start(); this.navControlService.start({ core }); - this.securityCheckupService.start({ securityOssStart: securityOss }); + this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks }); if (management) { this.managementService.start({ capabilities: core.application.capabilities }); } diff --git a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx index 6ba06e0cc4770..310caeac91dc1 100644 --- a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx +++ b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx @@ -16,13 +16,17 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { DocumentationLinksService } from '../documentation_links'; export const insecureClusterAlertTitle = i18n.translate( 'xpack.security.checkup.insecureClusterTitle', - { defaultMessage: 'Please secure your installation' } + { defaultMessage: 'Your data is not secure' } ); -export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) => +export const insecureClusterAlertText = ( + getDocLinksService: () => DocumentationLinksService, + onDismiss: (persist: boolean) => void +) => ((e) => { const AlertText = () => { const [persist, setPersist] = useState(false); @@ -33,7 +37,7 @@ export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) @@ -52,8 +56,9 @@ export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) size="s" color="primary" fill - href="https://www.elastic.co/what-is/elastic-stack-security" + href={getDocLinksService().getEnableSecurityDocUrl()} target="_blank" + data-test-subj="learnMoreButton" > {i18n.translate('xpack.security.checkup.enableButtonText', { defaultMessage: `Enable security`, diff --git a/x-pack/plugins/security/public/security_checkup/documentation_links.ts b/x-pack/plugins/security/public/security_checkup/documentation_links.ts new file mode 100644 index 0000000000000..b53a6ffd94be0 --- /dev/null +++ b/x-pack/plugins/security/public/security_checkup/documentation_links.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}`; + } + + public getEnableSecurityDocUrl() { + return `${this.esDocBasePath}/get-started-enable-security.html?blade=kibanasecuritymessage`; + } +} diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts index 3709f52d29ffb..691cbf8ac9ea1 100644 --- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts +++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MountPoint } from 'kibana/public'; + +import { docLinksServiceMock } from '../../../../../src/core/public/mocks'; import { mockSecurityOssPlugin } from '../../../../../src/plugins/security_oss/public/mocks'; import { insecureClusterAlertTitle } from './components'; import { SecurityCheckupService } from './security_checkup_service'; @@ -13,9 +16,12 @@ let mockOnDismiss = jest.fn(); jest.mock('./components', () => { return { insecureClusterAlertTitle: 'mock insecure cluster title', - insecureClusterAlertText: (onDismiss: any) => { + insecureClusterAlertText: (getDocLinksService: any, onDismiss: any) => { mockOnDismiss = onDismiss; - return 'mock insecure cluster text'; + const { insecureClusterAlertText } = jest.requireActual( + './components/insecure_cluster_alert' + ); + return insecureClusterAlertText(getDocLinksService, onDismiss); }, }; }); @@ -31,9 +37,7 @@ describe('SecurityCheckupService', () => { insecureClusterAlertTitle ); - expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledWith( - 'mock insecure cluster text' - ); + expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledTimes(1); }); }); describe('#start', () => { @@ -42,7 +46,7 @@ describe('SecurityCheckupService', () => { const securityOssStart = mockSecurityOssPlugin.createStart(); const service = new SecurityCheckupService(); service.setup({ securityOssSetup }); - service.start({ securityOssStart }); + service.start({ securityOssStart, docLinks: docLinksServiceMock.createStartContract() }); expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(0); @@ -50,5 +54,26 @@ describe('SecurityCheckupService', () => { expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(1); }); + + it('configures the doc link correctly', async () => { + const securityOssSetup = mockSecurityOssPlugin.createSetup(); + const securityOssStart = mockSecurityOssPlugin.createStart(); + const service = new SecurityCheckupService(); + service.setup({ securityOssSetup }); + service.start({ securityOssStart, docLinks: docLinksServiceMock.createStartContract() }); + + const [alertText] = securityOssSetup.insecureCluster.setAlertText.mock.calls[0]; + + const container = document.createElement('div'); + (alertText as MountPoint)(container); + + const docLink = container + .querySelector('[data-test-subj="learnMoreButton"]') + ?.getAttribute('href'); + + expect(docLink).toMatchInlineSnapshot( + `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/get-started-enable-security.html?blade=kibanasecuritymessage"` + ); + }); }); }); diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx index 899a74083656b..a0ea194170dff 100644 --- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx +++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DocLinksStart } from 'kibana/public'; + import { SecurityOssPluginSetup, SecurityOssPluginStart, } from '../../../../../src/plugins/security_oss/public'; import { insecureClusterAlertTitle, insecureClusterAlertText } from './components'; +import { DocumentationLinksService } from './documentation_links'; interface SetupDeps { securityOssSetup: SecurityOssPluginSetup; @@ -16,20 +19,27 @@ interface SetupDeps { interface StartDeps { securityOssStart: SecurityOssPluginStart; + docLinks: DocLinksStart; } export class SecurityCheckupService { private securityOssStart?: SecurityOssPluginStart; + private docLinksService?: DocumentationLinksService; + public setup({ securityOssSetup }: SetupDeps) { securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle); securityOssSetup.insecureCluster.setAlertText( - insecureClusterAlertText((persist: boolean) => this.onDismiss(persist)) + insecureClusterAlertText( + () => this.docLinksService!, + (persist: boolean) => this.onDismiss(persist) + ) ); } - public start({ securityOssStart }: StartDeps) { + public start({ securityOssStart, docLinks }: StartDeps) { this.securityOssStart = securityOssStart; + this.docLinksService = new DocumentationLinksService(docLinks); } private onDismiss(persist: boolean) { diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 6c3dcddcdb418..80b7dd35e595c 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -7,9 +7,11 @@ import { createConfig, ConfigSchema } from '../config'; import { loggingSystemMock } from 'src/core/server/mocks'; import { TypeOf } from '@kbn/config-schema'; -import { usageCollectionPluginMock } from 'src/plugins/usage_collection/server/mocks'; +import { + usageCollectionPluginMock, + createCollectorFetchContextMock, +} from 'src/plugins/usage_collection/server/mocks'; import { registerSecurityUsageCollector } from './security_usage_collector'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { SecurityLicenseFeatures } from '../../common/licensing'; @@ -34,7 +36,7 @@ describe('Security UsageCollector', () => { return license; }; - const clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + const collectorFetchContext = createCollectorFetchContextMock(); describe('initialization', () => { it('handles an undefined usage collector', () => { @@ -68,7 +70,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -89,7 +91,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -133,7 +135,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -182,7 +184,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -220,7 +222,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -258,7 +260,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -299,7 +301,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -338,7 +340,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -366,7 +368,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: true, @@ -392,7 +394,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -422,7 +424,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -450,7 +452,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 1a4852e450275..278ce1d39ae9f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -16,6 +16,7 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, } from '../../../lists/common/schemas'; +import { ESBoolQuery } from '../typed_json'; import { buildExceptionListQueries } from './build_exceptions_query'; import { Query as QueryString, @@ -31,7 +32,7 @@ export const getQueryFilter = ( index: Index, lists: Array, excludeExceptions: boolean = true -) => { +): ESBoolQuery => { const indexPattern: IIndexPattern = { fields: [], title: index.join(), diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 340f93150ce5c..08c544b9246e0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -111,6 +111,74 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R }; }; +/** + * Useful for e2e backend tests where it doesn't have date time and other + * server side properties attached to it. + */ +export const getThreatMatchingSchemaPartialMock = (): Partial => { + return { + author: [], + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 55, + risk_score_mapping: [], + name: 'Query with a rule id', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'threat_match', + threat: [], + version: 1, + exceptions_list: [], + actions: [], + throttle: 'no_actions', + query: 'user.name: root or user.name: admin', + language: 'kuery', + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], + }; +}; + export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { return { ...getRulesSchemaMock(anchorDate), diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index a6d59615794a6..1dd5668b3177a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -23,23 +23,6 @@ export const validateTree = { }), }; -/** - * Used to validate GET requests for non process events for a specific event. - */ -export const validateRelatedEvents = { - params: schema.object({ id: schema.string({ minLength: 1 }) }), - query: schema.object({ - events: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), - afterEvent: schema.maybe(schema.string()), - legacyEndpointID: schema.maybe(schema.string({ minLength: 1 })), - }), - body: schema.nullable( - schema.object({ - filter: schema.maybe(schema.string()), - }) - ), -}; - /** * Used to validate POST requests for `/resolver/events` api. */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 0054c1f1abdd5..510f1833b793b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -197,7 +197,6 @@ export interface SafeResolverTree { */ entityID: string; children: SafeResolverChildren; - relatedEvents: Omit; relatedAlerts: Omit; ancestry: SafeResolverAncestry; lifecycle: SafeResolverEvent[]; @@ -267,15 +266,6 @@ export interface ResolverRelatedEvents { nextEvent: string | null; } -/** - * Safe version of `ResolverRelatedEvents` - */ -export interface SafeResolverRelatedEvents { - entityID: string; - events: SafeResolverEvent[]; - nextEvent: string | null; -} - /** * Response structure for the events route. * `nextEvent` will be set to null when at the time of querying there were no more results to retrieve from ES. diff --git a/x-pack/plugins/security_solution/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index 61c1093002192..26832e23f6f2b 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -3,9 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DslQuery, Filter } from 'src/plugins/data/common'; + import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; -export type ESQuery = ESRangeQuery | ESQueryStringQuery | ESMatchQuery | ESTermQuery | JsonObject; +export type ESQuery = + | ESRangeQuery + | ESQueryStringQuery + | ESMatchQuery + | ESTermQuery + | ESBoolQuery + | JsonObject; export interface ESRangeQuery { range: { @@ -37,3 +45,12 @@ export interface ESQueryStringQuery { export interface ESTermQuery { term: Record; } + +export interface ESBoolQuery { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: never[]; + must_not: Filter[]; + }; +} diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index d8832dc4ee600..28889920e00e5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -135,7 +135,7 @@ describe('Custom detection rules creation', () => { // expect define step to repopulate cy.get(DEFINE_EDIT_BUTTON).click(); - cy.get(CUSTOM_QUERY_INPUT).should('have.text', newRule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', newRule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); @@ -182,7 +182,7 @@ describe('Custom detection rules creation', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); @@ -293,7 +293,7 @@ describe('Custom detection rules deletion and edition', () => { waitForKibana(); // expect define step to populate - cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.customQuery); if (existingRule.index && existingRule.index.length > 0) { cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join('')); } @@ -344,7 +344,7 @@ describe('Custom detection rules deletion and edition', () => { 'have.text', expectedEditedIndexPatterns.join('') ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', editedRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 5745a545f048b..252ffb6c8c660 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -145,7 +145,7 @@ describe.skip('Detection rules, EQL', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', eqlRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index 090012de72534..abc873f2df0ee 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -164,7 +164,7 @@ describe('Detection rules, override', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newOverrideRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 5ee7e69e877e3..9d988a46662fa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -143,7 +143,7 @@ describe('Detection rules, threshold', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newThresholdRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); getDetails(THRESHOLD_DETAILS).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index 9f61d11b7ac0f..8ce60450671b9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -45,7 +45,8 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Timelines', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79389 +describe.skip('Timelines', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 1433acd27c930..fa3c219595c72 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -190,7 +190,7 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( ) => { cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); cy.get(TIMELINE(rule.timelineId)).click(); - cy.get(CUSTOM_QUERY_INPUT).should('have.text', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -208,7 +208,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const threshold = 1; cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); - cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) .then((inputs) => { diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 079c34114dfd6..6573457c5f39a 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { TimelineId } from '../../../common/types/timeline'; @@ -44,9 +44,18 @@ interface HomePageProps { } const HomePageComponent: React.FC = ({ children }) => { - const { application } = useKibana().services; + const { application, overlays } = useKibana().services; const subPluginId = useRef(''); const { ref, height = 0 } = useThrottledResizeObserver(300); + const banners$ = overlays.banners.get$(); + const [headerFixed, setHeaderFixed] = useState(true); + const mainPaddingTop = headerFixed ? height : 0; + + useEffect(() => { + const subscription = banners$.subscribe((banners) => setHeaderFixed(!banners.length)); + return () => subscription.unsubscribe(); + }, [banners$]); // Only un/re-subscribe if the Observable changes + application.currentAppId$.subscribe((appId) => { subPluginId.current = appId ?? ''; }); @@ -72,9 +81,9 @@ const HomePageComponent: React.FC = ({ children }) => { return ( - + -
+
{indicesExist && showTimeline && ( diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 69bf2549d7439..4c8e87c4abfba 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -5,20 +5,10 @@ */ import React from 'react'; -import { Store, Action } from 'redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import { AppMountParameters } from '../../../../../src/core/public'; -import { State } from '../common/store'; -import { StartServices } from '../types'; import { SecurityApp } from './app'; -import { AppFrontendLibs } from '../common/lib/lib'; - -interface RenderAppProps extends AppFrontendLibs, AppMountParameters { - services: StartServices; - store: Store; - SubPluginRoutes: React.FC; -} +import { RenderAppProps } from './types'; export const renderApp = ({ apolloClient, @@ -27,7 +17,7 @@ export const renderApp = ({ services, store, SubPluginRoutes, -}: RenderAppProps) => { +}: RenderAppProps): (() => void) => { render( diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4590f05e12631..24ecf6b6d6cbb 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -8,12 +8,27 @@ import { Reducer, AnyAction, Middleware, + Action, + Store, Dispatch, PreloadedState, StateFromReducersMapObject, CombinedState, } from 'redux'; +import { AppMountParameters } from '../../../../../src/core/public'; +import { StartServices } from '../types'; +import { AppFrontendLibs } from '../common/lib/lib'; + +/** + * The React properties used to render `SecurityApp` as well as the `element` to render it into. + */ +export interface RenderAppProps extends AppFrontendLibs, AppMountParameters { + services: StartServices; + store: Store; + SubPluginRoutes: React.FC; +} + import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; @@ -31,7 +46,7 @@ export interface SecuritySubPlugin { storageTimelines?: Pick; } -type SecuritySubPluginKeyStore = +export type SecuritySubPluginKeyStore = | 'hosts' | 'network' | 'timeline' diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 14c42697dcbb4..3e3d21b9926d1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -12,7 +12,6 @@ import { CommentRequest } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; @@ -61,10 +60,7 @@ export const AddComment = React.memo( setFieldValue, ]); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - comment, - onCommentChange - ); + const { handleCursorChange } = useInsertTimeline(comment, onCommentChange); const addQuote = useCallback( (quote) => { @@ -116,13 +112,6 @@ export const AddComment = React.memo( {i18n.ADD_COMMENT} ), - topRightContent: ( - - ), }} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index b7a80bcf6633c..42633c5d2ccf8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -28,7 +28,6 @@ import { } from '../../../shared_imports'; import { usePostCase } from '../../containers/use_post_case'; import { schema, FormProps } from './schema'; -import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; import { useGetTags } from '../../containers/use_get_tags'; @@ -136,10 +135,7 @@ export const Create = React.memo(() => { setFieldValue, ]); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - description, - onDescriptionChange - ); + const { handleCursorChange } = useInsertTimeline(description, onDescriptionChange); const handleTimelineClick = useTimelineClick(); @@ -221,20 +217,13 @@ export const Create = React.memo(() => { isDisabled: isLoading, onClickTimeline: handleTimelineClick, onCursorPositionUpdate: handleCursorChange, - topRightContent: ( - - ), }} /> ), }), - [isLoading, options, handleCursorChange, handleTimelineClick, handleOnTimelineChange] + [isLoading, options, handleCursorChange, handleTimelineClick] ); const secondStep = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 0c6a54d4434d2..11623e1367574 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -24,13 +24,13 @@ import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/c import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header` - ${({ theme }) => ` +const Wrapper = styled.header<{ $isFixed: boolean }>` + ${({ theme, $isFixed }) => ` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; width: 100%; z-index: ${theme.eui.euiZNavigation}; - position: fixed; + position: ${$isFixed ? 'fixed' : 'relative'}; `} `; Wrapper.displayName = 'Wrapper'; @@ -62,75 +62,78 @@ FlexGroup.displayName = 'FlexGroup'; interface HeaderGlobalProps { hideDetectionEngine?: boolean; + isFixed?: boolean; } export const HeaderGlobal = React.memo( - forwardRef(({ hideDetectionEngine = false }, ref) => { - const { globalHeaderPortalNode } = useGlobalHeaderPortal(); - const { globalFullScreen } = useFullScreen(); - const search = useGetUrlSearch(navTabs.overview); - const { application, http } = useKibana().services; - const { navigateToApp } = application; - const basePath = http.basePath.get(); - const goToOverview = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); - }, - [navigateToApp, search] - ); - return ( - - - - - - - - - - - - - key !== SecurityPageName.detections, navTabs) - : navTabs - } - /> - - - - - - {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + forwardRef( + ({ hideDetectionEngine = false, isFixed = true }, ref) => { + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + const { globalFullScreen } = useFullScreen(); + const search = useGetUrlSearch(navTabs.overview); + const { application, http } = useKibana().services; + const { navigateToApp } = application; + const basePath = http.basePath.get(); + const goToOverview = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); + }, + [navigateToApp, search] + ); + return ( + + + + + - + + + + + + + key !== SecurityPageName.detections, navTabs) + : navTabs + } + /> - )} + + + + + {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + + + + )} - - - {i18n.BUTTON_ADD_DATA} - - - - - - - - - ); - }) + + + {i18n.BUTTON_ADD_DATA} + + + + + + + + + ); + } + ) ); HeaderGlobal.displayName = 'HeaderGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index 61e9dd04d910d..4bd2cd05d49d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -200,11 +200,7 @@ exports[`item_details_card ItemDetailsPropertySummary should render correctly 1` name 1 - - value 1 - + value 1 `; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 9105514b75807..c41c5f89c0068 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -66,16 +66,13 @@ const DescriptionListDescription = styled(EuiDescriptionListDescription)` interface ItemDetailsPropertySummaryProps { name: ReactNode | ReactNode[]; value: ReactNode | ReactNode[]; - title?: string; } -export const ItemDetailsPropertySummary: FC = memo( - ({ name, value, title = '' }) => ( +export const ItemDetailsPropertySummary = memo( + ({ name, value }) => ( <> {name} - - {value} - + {value} ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 7395100784d51..e7d7e60a3c408 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -34,7 +34,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & defaultStackByOption: MatrixHistogramOption; errorMessage: string; headerChildren?: React.ReactNode; - footerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; histogramType: MatrixHistogramType; id: string; @@ -48,7 +47,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & subtitle?: string | GetSubTitle; timelineId?: string; title: string | GetTitle; - yTitle?: string | undefined; }; const DEFAULT_PANEL_HEIGHT = 300; @@ -70,7 +68,6 @@ export const MatrixHistogramComponent: React.FC = errorMessage, filterQuery, headerChildren, - footerChildren, histogramType, hideHistogramIfEmpty = false, id, @@ -89,7 +86,6 @@ export const MatrixHistogramComponent: React.FC = title, titleSize, yTickFormatter, - yTitle, }) => { const dispatch = useDispatch(); const handleBrushEnd = useCallback( @@ -118,18 +114,8 @@ export const MatrixHistogramComponent: React.FC = onBrushEnd: handleBrushEnd, yTickFormatter, showLegend, - yTitle, }), - [ - chartHeight, - startDate, - legendPosition, - endDate, - handleBrushEnd, - yTickFormatter, - showLegend, - yTitle, - ] + [chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend] ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState( @@ -243,11 +229,6 @@ export const MatrixHistogramComponent: React.FC = timelineId={timelineId} /> )} - {footerChildren != null && ( - - {footerChildren} - - )} {showSpacer && } diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 4c04a4cca9f82..828cadd90bb13 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -71,6 +71,7 @@ export interface MatrixHistogramQueryProps { startDate: string; histogramType: MatrixHistogramType; threshold?: { field: string | undefined; value: number } | undefined; + skip?: boolean; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { @@ -105,7 +106,6 @@ export interface BarchartConfigs { yTickFormatter: TickFormatter; tickSize: number; }; - yAxisTitle: string | undefined; settings: { legendPosition: Position; onBrushEnd: UpdateDateRange; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts index d1af29d7da27c..5b5b56cf0ec45 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts @@ -19,7 +19,6 @@ interface GetBarchartConfigsProps { onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; - yTitle?: string | undefined; } export const DEFAULT_CHART_HEIGHT = 174; @@ -33,7 +32,6 @@ export const getBarchartConfigs = ({ onBrushEnd, yTickFormatter, showLegend, - yTitle, }: GetBarchartConfigsProps): BarchartConfigs => ({ series: { xScaleType: ScaleType.Time, @@ -45,7 +43,6 @@ export const getBarchartConfigs = ({ yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, tickSize: 8, }, - yAxisTitle: yTitle, settings: { legendPosition: legendPosition ?? Position.Right, onBrushEnd, diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 2696b115cdc18..54e6e1cdc1185 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { SourcererScopeName } from '../../store/sourcerer/model'; -import { SourcererComponent } from './index'; +import { Sourcerer } from './index'; import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { sourcererActions, sourcererModel } from '../../store/sourcerer'; import { @@ -75,7 +75,7 @@ describe('Sourcerer component', () => { it('Mounts with all options selected', () => { const wrapper = mount( - + ); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); @@ -108,7 +108,7 @@ describe('Sourcerer component', () => { ); const wrapper = mount( - + ); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); @@ -119,12 +119,13 @@ describe('Sourcerer component', () => { it('onChange calls updateSourcererScopeIndices', async () => { const wrapper = mount( - + ); - expect(true).toBeTruthy(); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - + expect( + wrapper.find(`[data-test-subj="sourcerer-popover"]`).first().prop('isOpen') + ).toBeTruthy(); await waitFor(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { onChange: (a: EuiComboBoxOptionOption[]) => void; @@ -132,6 +133,7 @@ describe('Sourcerer component', () => { wrapper.update(); }); wrapper.find(`[data-test-subj="add-index"]`).first().simulate('click'); + expect(wrapper.find(`[data-test-subj="sourcerer-popover"]`).first().prop('isOpen')).toBeFalsy(); expect(mockDispatch).toHaveBeenCalledWith( sourcererActions.setSelectedIndexPatterns({ @@ -140,4 +142,110 @@ describe('Sourcerer component', () => { }) ); }); + it('resets to config index patterns', async () => { + store = createStore( + { + ...state, + sourcerer: { + ...state.sourcerer, + kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }], + configIndexPatterns: ['packetbeat-*'], + }, + }, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy(); + wrapper + .find( + `[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton` + ) + .first() + .simulate('click'); + expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeTruthy(); + wrapper.find(`[data-test-subj="sourcerer-reset"]`).first().simulate('click'); + expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy(); + }); + it('returns index pattern options for kibanaIndexPatterns and configIndexPatterns', () => { + store = createStore( + { + ...state, + sourcerer: { + ...state.sourcerer, + kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }], + configIndexPatterns: ['packetbeat-*'], + }, + }, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy(); + wrapper + .find( + `[data-test-subj="indexPattern-switcher"] [title="auditbeat-*"] button.euiBadge__iconButton` + ) + .first() + .simulate('click'); + wrapper.update(); + expect(wrapper.find(`[data-test-subj="kip-option"]`).first().text()).toEqual(' auditbeat-*'); + wrapper + .find( + `[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton` + ) + .first() + .simulate('click'); + wrapper.update(); + expect(wrapper.find(`[data-test-subj="config-option"]`).first().text()).toEqual('packetbeat-*'); + }); + it('combines index pattern options for kibanaIndexPatterns and configIndexPatterns', () => { + store = createStore( + { + ...state, + sourcerer: { + ...state.sourcerer, + kibanaIndexPatterns: [ + { id: '1234', title: 'auditbeat-*' }, + { id: '5678', title: 'packetbeat-*' }, + ], + configIndexPatterns: ['packetbeat-*'], + }, + }, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper + .find( + `[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton` + ) + .first() + .simulate('click'); + wrapper.update(); + expect( + wrapper.find(`[title="packetbeat-*"] [data-test-subj="kip-option"]`).first().text() + ).toEqual(' packetbeat-*'); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 7a74f5bf2247f..bc09116798344 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -24,7 +24,6 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import * as i18n from './translations'; -import { SOURCERER_FEATURE_FLAG_ON } from '../../containers/sourcerer/constants'; import { sourcererActions, sourcererModel } from '../../store/sourcerer'; import { State } from '../../store'; import { getSourcererScopeSelector, SourcererScopeSelector } from './selectors'; @@ -40,7 +39,7 @@ interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; } -export const SourcererComponent = React.memo(({ scope: scopeId }) => { +export const Sourcerer = React.memo(({ scope: scopeId }) => { const dispatch = useDispatch(); const sourcererScopeSelector = useMemo(getSourcererScopeSelector, []); const { configIndexPatterns, kibanaIndexPatterns, sourcererScope } = useSelector< @@ -71,17 +70,14 @@ export const SourcererComponent = React.memo(({ scope: ); const renderOption = useCallback( - (option) => { - const { value } = option; - if (kibanaIndexPatterns.some((kip) => kip.title === value)) { - return ( - <> - {value} - - ); - } - return <>{value}; - }, + ({ value }) => + kibanaIndexPatterns.some((kip) => kip.title === value) ? ( + + {value} + + ) : ( + {value} + ), [kibanaIndexPatterns] ); @@ -175,11 +171,13 @@ export const SourcererComponent = React.memo(({ scope: return ( @@ -221,6 +219,4 @@ export const SourcererComponent = React.memo(({ scope: ); }); -SourcererComponent.displayName = 'Sourcerer'; - -export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? SourcererComponent : () => null; +Sourcerer.displayName = 'Sourcerer'; diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/text_field_value/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1e34c1daeea31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/__snapshots__/index.test.tsx.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`text_field_value TextFieldValue should render long text correctly, when there is limit 1`] = ` + + + field 1 + + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + + } + delay="regular" + position="top" +> + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 sup... + + +`; + +exports[`text_field_value TextFieldValue should render long text correctly, when there is no limit 1`] = ` + + + field 1 + + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + + } + delay="regular" + position="top" +> + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + +`; + +exports[`text_field_value TextFieldValue should render small text correctly, when there is limit 1`] = ` + + + field 1 + + + value 1 + + + } + delay="regular" + position="top" +> + + value 1 + + +`; + +exports[`text_field_value TextFieldValue should render small text correctly, when there is no limit 1`] = ` + + + field 1 + + + value 1 + + + } + delay="regular" + position="top" +> + + value 1 + + +`; diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx new file mode 100644 index 0000000000000..cd0a4fcd65610 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TextFieldValue } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +const longText = [...new Array(20).keys()].map((i) => ` super long text part ${i}`).join(' '); + +storiesOf('Components/TextFieldValue', module) + .add('short text, no limit', () => ) + .add('short text, with limit', () => ( + + )) + .add('long text, no limit', () => ) + .add('long text, with limit', () => ( + + )); diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx new file mode 100644 index 0000000000000..3ea1ae6d05ad2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TextFieldValue } from '.'; + +describe('text_field_value', () => { + describe('TextFieldValue', () => { + const longText = [...new Array(20).keys()].map((i) => ` super long text part ${i}`).join(' '); + + it('should render small text correctly, when there is no limit', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render small text correctly, when there is limit', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + + it('should render long text correctly, when there is no limit', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render long text correctly, when there is limit', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx new file mode 100644 index 0000000000000..8b482215f24fd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; + +const trimTextOverflow = (text: string, maxLength?: number) => { + if (maxLength !== undefined && text.length > maxLength) { + return `${text.substr(0, maxLength)}...`; + } else { + return text; + } +}; + +interface Props { + fieldName: string; + value: string; + maxLength?: number; + className?: string; +} + +/* + * Component to display text field value. Text field values can be large and need + * programmatic truncation to a fixed text length. As text can be truncated the tooltip + * is shown displaying the field name and full value. If the use case allows single + * line truncation with CSS use eui-textTruncate class on this component instead of + * maxLength property. + */ +export const TextFieldValue = ({ fieldName, value, maxLength, className }: Props) => { + return ( + + {fieldName} + {value} + + } + > + {trimTextOverflow(value, maxLength)} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 7c6a110f56b81..6250a4fd959b6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -27,6 +27,13 @@ import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +export type Buckets = Array<{ + key: string; + doc_count: number; +}>; + +const bucketEmpty: Buckets = []; + export interface UseMatrixHistogramArgs { data: MatrixHistogramData[]; inspect: InspectResponse; @@ -49,7 +56,12 @@ export const useMatrixHistogram = ({ stackByField, startDate, threshold, -}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => { + skip = false, +}: MatrixHistogramQueryProps): [ + boolean, + UseMatrixHistogramArgs, + (to: string, from: string) => void +] => { const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); @@ -98,10 +110,11 @@ export const useMatrixHistogram = ({ next: (response) => { if (isCompleteResponse(response)) { if (!didCancel) { - const histogramBuckets: Array<{ - key: string; - doc_count: number; - }> = getOr([], 'rawResponse.aggregations.eventActionGroup.buckets', response); + const histogramBuckets: Buckets = getOr( + bucketEmpty, + 'rawResponse.aggregations.eventActionGroup.buckets', + response + ); setLoading(false); setMatrixHistogramResponse((prevResponse) => ({ ...prevResponse, @@ -123,10 +136,12 @@ export const useMatrixHistogram = ({ } }, error: (msg) => { + if (!didCancel) { + setLoading(false); + } if (!(msg instanceof AbortError)) { - notifications.toasts.addDanger({ + notifications.toasts.addError(msg, { title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM, - text: msg.message, }); } }, @@ -166,8 +181,24 @@ export const useMatrixHistogram = ({ }, [indexNames, endDate, filterQuery, startDate, stackByField, histogramType, threshold]); useEffect(() => { - hostsSearch(matrixHistogramRequest); - }, [matrixHistogramRequest, hostsSearch]); + if (!skip) { + hostsSearch(matrixHistogramRequest); + } + }, [matrixHistogramRequest, hostsSearch, skip]); + + const runMatrixHistogramSearch = useCallback( + (to: string, from: string) => { + hostsSearch({ + ...matrixHistogramRequest, + timerange: { + interval: '12h', + from, + to, + }, + }); + }, + [matrixHistogramRequest, hostsSearch] + ); - return [loading, matrixHistogramResponse]; + return [loading, matrixHistogramResponse, runMatrixHistogramSearch]; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 673db7af2b5e6..accfb38bc3dc1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -16,6 +16,10 @@ import { mockPatterns, mockSource } from './mocks'; import { RouteSpyState } from '../../utils/route/types'; import { SecurityPageName } from '../../../../common/constants'; import { createStore, State } from '../../store'; +import { + useUserInfo, + initialState as userInfoState, +} from '../../../detections/components/user_info'; import { apolloClientObservable, createSecuritySolutionStorageMock, @@ -23,6 +27,7 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../mock'; +import { SourcererScopeName } from '../../store/sourcerer/model'; const mockSourceDefaults = mockSource; const mockRouteSpy: RouteSpyState = { @@ -33,6 +38,8 @@ const mockRouteSpy: RouteSpyState = { pathName: '/', }; const mockDispatch = jest.fn(); +const mockUseUserInfo = useUserInfo as jest.Mock; +jest.mock('../../../detections/components/user_info'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -79,12 +86,6 @@ jest.mock('../../utils/apollo_context', () => ({ })); describe('Sourcerer Hooks', () => { - // const testId = SourcererScopeName.default; - // const uninitializedId = SourcererScopeName.detections; - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); let store = createStore( @@ -96,6 +97,8 @@ describe('Sourcerer Hooks', () => { ); beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); store = createStore( state, SUB_PLUGINS_REDUCER, @@ -103,169 +106,67 @@ describe('Sourcerer Hooks', () => { kibanaObservable, storage ); + mockUseUserInfo.mockImplementation(() => userInfoState); + }); + it('initializes loading default and timeline index patterns', async () => { + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + expect(mockDispatch).toBeCalledTimes(2); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { id: 'default', loading: true }, + }); + expect(mockDispatch.mock.calls[1][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { id: 'timeline', loading: true }, + }); + }); + }); + it('sets signal index name', async () => { + await act(async () => { + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + loading: false, + signalIndexName: 'signals-*', + })); + const { rerender, waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); + rerender(); + expect(mockDispatch.mock.calls[2][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME', + payload: { signalIndexName: 'signals-*' }, + }); + expect(mockDispatch.mock.calls[3][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_INDEX_PATTERNS', + payload: { id: 'timeline', selectedPatterns: ['signals-*'] }, + }); + }); }); - describe('Initialization', () => { - it('initializes loading default and timeline index patterns', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + it('handles detections page', async () => { + await act(async () => { + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + signalIndexName: 'signals-*', + isSignalIndexExists: true, + })); + const { rerender, waitForNextUpdate } = renderHook( + () => useInitSourcerer(SourcererScopeName.detections), + { wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(mockDispatch).toBeCalledTimes(2); - expect(mockDispatch.mock.calls[0][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', - payload: { id: 'default', loading: true }, - }); - expect(mockDispatch.mock.calls[1][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', - payload: { id: 'timeline', loading: true }, - }); - // expect(mockDispatch.mock.calls[1][0]).toEqual({ - // type: 'x-pack/security_solution/local/sourcerer/SET_INDEX_PATTERNS_LIST', - // payload: { allIndexPatterns: mockPatterns, kibanaIndexPatterns: [] }, - // }); + } + ); + await waitForNextUpdate(); + rerender(); + expect(mockDispatch.mock.calls[1][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_INDEX_PATTERNS', + payload: { id: 'detections', selectedPatterns: ['signals-*'] }, }); }); - // TO DO sourcerer @S - // it('initializes loading default source group', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // expect(result.current).toEqual({ - // activeSourcererScopeId: 'default', - // kibanaIndexPatterns: mockPatterns, - // isIndexPatternsLoading: false, - // getSourcererScopeById: result.current.getSourcererScopeById, - // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId, - // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices, - // }); - // }); - // }); - // it('initialize completes with formatted source group data', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // expect(result.current).toEqual({ - // activeSourcererScopeId: testId, - // kibanaIndexPatterns: mockPatterns, - // isIndexPatternsLoading: false, - // getSourcererScopeById: result.current.getSourcererScopeById, - // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId, - // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices, - // }); - // }); - // }); }); - // describe('Methods', () => { - // it('getSourcererScopeById: initialized source group returns defaults', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // const initializedSourcererScope = result.current.getSourcererScopeById(testId); - // expect(initializedSourcererScope).toEqual(mockSourcererScope(testId)); - // }); - // }); - // it('getSourcererScopeById: uninitialized source group returns defaults', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // const uninitializedSourcererScope = result.current.getSourcererScopeById(uninitializedId); - // expect(uninitializedSourcererScope).toEqual( - // getSourceDefaults(uninitializedId, mockPatterns) - // ); - // }); - // }); - // // it('initializeSourcererScope: initializes source group', async () => { - // // await act(async () => { - // // const { result, waitForNextUpdate } = renderHook( - // // () => useSourcerer(), - // // { - // // wrapper: ({ children }) => {children}, - // // } - // // ); - // // await waitForNextUpdate(); - // // await waitForNextUpdate(); - // // await waitForNextUpdate(); - // // result.current.initializeSourcererScope( - // // uninitializedId, - // // mockSourcererScopes[uninitializedId], - // // true - // // ); - // // await waitForNextUpdate(); - // // const initializedSourcererScope = result.current.getSourcererScopeById(uninitializedId); - // // expect(initializedSourcererScope.selectedPatterns).toEqual( - // // mockSourcererScopes[uninitializedId] - // // ); - // // }); - // // }); - // it('setActiveSourcererScopeId: active source group id gets set only if it gets initialized first', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // expect(result.current.activeSourcererScopeId).toEqual(testId); - // result.current.setActiveSourcererScopeId(uninitializedId); - // expect(result.current.activeSourcererScopeId).toEqual(testId); - // // result.current.initializeSourcererScope(uninitializedId); - // result.current.setActiveSourcererScopeId(uninitializedId); - // expect(result.current.activeSourcererScopeId).toEqual(uninitializedId); - // }); - // }); - // it('updateSourcererScopeIndices: updates source group indices', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook( - // () => useInitSourcerer(), - // { - // wrapper: ({ children }) => {children}, - // } - // ); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // await waitForNextUpdate(); - // let sourceGroup = result.current.getSourcererScopeById(testId); - // expect(sourceGroup.selectedPatterns).toEqual(mockSourcererScopes[testId]); - // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]); - // result.current.updateSourcererScopeIndices({ - // id: testId, - // selectedPatterns: ['endgame-*', 'filebeat-*'], - // }); - // await waitForNextUpdate(); - // sourceGroup = result.current.getSourcererScopeById(testId); - // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]); - // expect(sourceGroup.selectedPatterns).toEqual(['endgame-*', 'filebeat-*']); - // }); - // }); - // }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts index 80b8b99169465..fb2e484c0e3f1 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Unit } from '@elastic/datemath'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { @@ -16,10 +15,6 @@ import { isErrorResponse, isValidationErrorResponse, } from '../../../../common/search_strategy/eql'; -import { getEqlAggsData, getSequenceAggs } from './helpers'; -import { EqlPreviewResponse, Source } from './types'; -import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; -import { EqlSearchResponse } from '../../../../common/detection_engine/types'; interface Params { index: string[]; @@ -56,66 +51,3 @@ export const validateEql = async ({ return { valid: true, errors: [] }; } }; - -interface AggsParams { - data: DataPublicPluginStart; - index: string[]; - interval: Unit; - fromTime: string; - query: string; - toTime: string; - signal: AbortSignal; -} - -export const getEqlPreview = async ({ - data, - index, - interval, - query, - fromTime, - toTime, - signal, -}: AggsParams): Promise => { - try { - const response = await data.search - .search>>( - { - params: { - // @ts-expect-error allow_no_indices is missing on EqlSearch - allow_no_indices: true, - index: index.join(), - body: { - filter: { - range: { - '@timestamp': { - gte: toTime, - lte: fromTime, - format: 'strict_date_optional_time', - }, - }, - }, - query, - // EQL requires a cap, otherwise it defaults to 10 - // It also sorts on ascending order, capping it at - // something smaller like 20, made it so that some of - // the more recent events weren't returned - size: 100, - }, - }, - }, - { - strategy: 'eql', - abortSignal: signal, - } - ) - .toPromise(); - - if (hasEqlSequenceQuery(query)) { - return getSequenceAggs(response, interval, toTime, fromTime); - } else { - return getEqlAggsData(response, interval, toTime, fromTime); - } - } catch (err) { - throw new Error(JSON.stringify(err)); - } -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts index 1418c1155877b..07e8caa0bf0b9 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import dateMath from '@elastic/datemath'; +import moment from 'moment'; import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { Source } from './types'; import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { inputsModel } from '../../../common/store'; import { calculateBucketForHour, @@ -18,7 +20,7 @@ import { getSequenceAggs, } from './helpers'; -const getMockResponse = (): EqlSearchStrategyResponse> => +export const getMockResponse = (): EqlSearchStrategyResponse> => ({ id: 'some-id', rawResponse: { @@ -129,6 +131,17 @@ const getMockSequenceResponse = (): EqlSearchStrategyResponse { describe('calculateBucketForHour', () => { - test('returns 2 if event occured within 2 minutes of "now"', () => { + test('returns 2 if event occurred within 2 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-1m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -151,7 +164,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(2); }); - test('returns 10 if event occured within 8-10 minutes of "now"', () => { + test('returns 10 if event occurred within 8-10 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-9m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -160,7 +173,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(10); }); - test('returns 16 if event occured within 10-15 minutes of "now"', () => { + test('returns 16 if event occurred within 10-15 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-15m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -169,7 +182,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(16); }); - test('returns 60 if event occured within 58-60 minutes of "now"', () => { + test('returns 60 if event occurred within 58-60 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-59m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -207,7 +220,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(0); }); - test('returns 1 if event occured within 60 minutes of "now"', () => { + test('returns 1 if event occurred within 60 minutes of "now"', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-40m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -216,7 +229,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(1); }); - test('returns 2 if event occured 60-120 minutes from "now"', () => { + test('returns 2 if event occurred 60-120 minutes from "now"', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-120m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -225,7 +238,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(2); }); - test('returns 3 if event occured 120-180 minutes from "now', () => { + test('returns 3 if event occurred 120-180 minutes from "now', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-121m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -234,7 +247,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(3); }); - test('returns 4 if event occured 180-240 minutes from "now', () => { + test('returns 4 if event occurred 180-240 minutes from "now', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-220m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -245,16 +258,22 @@ describe('eql/helpers', () => { }); describe('getEqlAggsData', () => { - test('it returns results bucketed into 5 min intervals when range is "h"', () => { + test('it returns results bucketed into 2 min intervals when range is "h"', () => { const mockResponse = getMockResponse(); const aggs = getEqlAggsData( mockResponse, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); + const date1 = moment(aggs.data[0].x); + const date2 = moment(aggs.data[1].x); + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); expect(aggs.data).toHaveLength(31); expect(aggs.data).toEqual([ { g: 'hits', x: 1601827200368, y: 0 }, @@ -345,10 +364,15 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( response, 'd', - '2020-10-03T23:50:00.368707900Z', - '2020-10-04T23:50:00.368707900Z' + '2020-10-04T23:50:00.368707900Z', + jest.fn() as inputsModel.Refetch ); + const date1 = moment(aggs.data[0].x); + const date2 = moment(aggs.data[1].x); + // This'll be in ms + const diff = date1.diff(date2); + expect(diff).toEqual(3600000); expect(aggs.data).toHaveLength(25); expect(aggs.data).toEqual([ { g: 'hits', x: 1601855400368, y: 0 }, @@ -385,8 +409,8 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( mockResponse, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); expect(aggs.totalCount).toEqual(4); @@ -417,53 +441,12 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( response, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); - expect(aggs).toEqual({ - data: [ - { g: 'hits', x: 1601827200368, y: 0 }, - { g: 'hits', x: 1601827080368, y: 0 }, - { g: 'hits', x: 1601826960368, y: 0 }, - { g: 'hits', x: 1601826840368, y: 0 }, - { g: 'hits', x: 1601826720368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 0 }, - { g: 'hits', x: 1601826480368, y: 0 }, - { g: 'hits', x: 1601826360368, y: 0 }, - { g: 'hits', x: 1601826240368, y: 0 }, - { g: 'hits', x: 1601826120368, y: 0 }, - { g: 'hits', x: 1601826000368, y: 0 }, - { g: 'hits', x: 1601825880368, y: 0 }, - { g: 'hits', x: 1601825760368, y: 0 }, - { g: 'hits', x: 1601825640368, y: 0 }, - { g: 'hits', x: 1601825520368, y: 0 }, - { g: 'hits', x: 1601825400368, y: 0 }, - { g: 'hits', x: 1601825280368, y: 0 }, - { g: 'hits', x: 1601825160368, y: 0 }, - { g: 'hits', x: 1601825040368, y: 0 }, - { g: 'hits', x: 1601824920368, y: 0 }, - { g: 'hits', x: 1601824800368, y: 0 }, - { g: 'hits', x: 1601824680368, y: 0 }, - { g: 'hits', x: 1601824560368, y: 0 }, - { g: 'hits', x: 1601824440368, y: 0 }, - { g: 'hits', x: 1601824320368, y: 0 }, - { g: 'hits', x: 1601824200368, y: 0 }, - { g: 'hits', x: 1601824080368, y: 0 }, - { g: 'hits', x: 1601823960368, y: 0 }, - { g: 'hits', x: 1601823840368, y: 0 }, - { g: 'hits', x: 1601823720368, y: 0 }, - { g: 'hits', x: 1601823600368, y: 0 }, - ], - gte: '2020-10-04T15:00:00.368707900Z', - inspect: { - dsl: [JSON.stringify(response.rawResponse.meta.request.params, null, 2)], - response: [JSON.stringify(response.rawResponse.body, null, 2)], - }, - lte: '2020-10-04T16:00:00.368707900Z', - totalCount: 0, - warnings: [], - }); + expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy(); + expect(aggs.totalCount).toEqual(0); }); }); @@ -510,7 +493,7 @@ describe('eql/helpers', () => { ]); }); - test('returns array of 30 numbers from start param to end param if multiplier is 1', () => { + test('returns array of numbers from start param to end param if multiplier is 1', () => { const arrayOfNumbers = createIntervalArray(0, 12, 1); expect(arrayOfNumbers).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); }); @@ -518,8 +501,15 @@ describe('eql/helpers', () => { describe('getInterval', () => { test('returns object with 2 minute interval keys if range is "h"', () => { - const intervals = getInterval('h', 1601856270140); + const intervals = getInterval('h', Date.parse('2020-10-04T15:00:00.368707900Z')); const keys = Object.keys(intervals); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['2'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); expect(keys).toEqual([ '0', '2', @@ -557,40 +547,13 @@ describe('eql/helpers', () => { test('returns object with 2 minute interval timestamps if range is "h"', () => { const intervals = getInterval('h', 1601856270140); - const timestamps = Object.keys(intervals).map((key) => intervals[key].timestamp); - expect(timestamps).toEqual([ - '1601856270140', - '1601856150140', - '1601856030140', - '1601855910140', - '1601855790140', - '1601855670140', - '1601855550140', - '1601855430140', - '1601855310140', - '1601855190140', - '1601855070140', - '1601854950140', - '1601854830140', - '1601854710140', - '1601854590140', - '1601854470140', - '1601854350140', - '1601854230140', - '1601854110140', - '1601853990140', - '1601853870140', - '1601853750140', - '1601853630140', - '1601853510140', - '1601853390140', - '1601853270140', - '1601853150140', - '1601853030140', - '1601852910140', - '1601852790140', - '1601852670140', - ]); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['2'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); }); test('returns object with 1 hour interval keys if range is "d"', () => { @@ -627,34 +590,13 @@ describe('eql/helpers', () => { test('returns object with 1 hour interval timestamps if range is "d"', () => { const intervals = getInterval('d', 1601856270140); - const timestamps = Object.keys(intervals).map((key) => intervals[key].timestamp); - expect(timestamps).toEqual([ - '1601856270140', - '1601852670140', - '1601849070140', - '1601845470140', - '1601841870140', - '1601838270140', - '1601834670140', - '1601831070140', - '1601827470140', - '1601823870140', - '1601820270140', - '1601816670140', - '1601813070140', - '1601809470140', - '1601805870140', - '1601802270140', - '1601798670140', - '1601795070140', - '1601791470140', - '1601787870140', - '1601784270140', - '1601780670140', - '1601777070140', - '1601773470140', - '1601769870140', - ]); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['1'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(3600000); }); test('returns error if range is anything other than "h" or "d"', () => { @@ -665,12 +607,7 @@ describe('eql/helpers', () => { describe('getSequenceAggs', () => { test('it aggregates events by sequences', () => { const mockResponse = getMockSequenceResponse(); - const sequenceAggs = getSequenceAggs( - mockResponse, - 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' - ); + const sequenceAggs = getSequenceAggs(mockResponse, jest.fn() as inputsModel.Refetch); expect(sequenceAggs.data).toEqual([ { g: 'Seq. 1', x: '2020-10-04T15:16:54.368707900Z', y: 1 }, diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts index 0b2eba33b93d6..4b5986d966df3 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts @@ -5,13 +5,12 @@ */ import moment from 'moment'; import { Unit } from '@elastic/datemath'; +import { inputsModel } from '../../../common/store'; -import * as i18n from '../../../detections/components/rules/query_preview/translations'; import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { InspectResponse } from '../../../types'; import { EqlPreviewResponse, Source } from './types'; import { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import { HITS_THRESHOLD } from '../../../detections/components/rules/query_preview/helpers'; type EqlAggBuckets = Record; @@ -33,37 +32,16 @@ export const calculateBucketForDay = (eventTimestamp: number, relativeNow: numbe return Math.ceil(minutes / 60); }; -export const constructWarnings = (timestampIssue: boolean, hits: number, range: Unit): string[] => { - let warnings: string[] = []; - - if (timestampIssue) { - warnings = [i18n.PREVIEW_WARNING_TIMESTAMP]; - } - - if (hits === EQL_QUERY_EVENT_SIZE) { - warnings = [...warnings, i18n.PREVIEW_WARNING_CAP_HIT(EQL_QUERY_EVENT_SIZE)]; - } - - if (hits > HITS_THRESHOLD[range]) { - warnings = [...warnings, i18n.QUERY_PREVIEW_NOISE_WARNING]; - } - - return warnings; -}; - export const formatInspect = ( response: EqlSearchStrategyResponse> ): InspectResponse => { - if (response != null) { - return { - dsl: [JSON.stringify(response.rawResponse.meta.request.params, null, 2)] ?? [], - response: [JSON.stringify(response.rawResponse.body, null, 2)] ?? [], - }; - } - + const body = response.rawResponse.meta.request.params.body; + const bodyParse = typeof body === 'string' ? JSON.parse(body) : body; return { - dsl: [], - response: [], + dsl: [ + JSON.stringify({ ...response.rawResponse.meta.request.params, body: bodyParse }, null, 2), + ], + response: [JSON.stringify(response.rawResponse.body, null, 2)], }; }; @@ -74,24 +52,22 @@ export const getEqlAggsData = ( response: EqlSearchStrategyResponse>, range: Unit, to: string, - from: string + refetch: inputsModel.Refetch ): EqlPreviewResponse => { const { dsl, response: inspectResponse } = formatInspect(response); // The upper bound of the timestamps - const relativeNow: number = Date.parse(from); - const accumulator: EqlAggBuckets = getInterval(range, relativeNow); + const relativeNow = Date.parse(to); + const accumulator = getInterval(range, relativeNow); const events = response.rawResponse.body.hits.events ?? []; const totalCount = response.rawResponse.body.hits.total.value; - let timestampNotFound = false; const buckets = events.reduce((acc, hit) => { const timestamp = hit._source['@timestamp']; if (timestamp == null) { - timestampNotFound = true; return acc; } - const eventTimestamp: number = Date.parse(timestamp); + const eventTimestamp = Date.parse(timestamp); const bucket = range === 'h' ? calculateBucketForHour(eventTimestamp, relativeNow) @@ -107,18 +83,14 @@ export const getEqlAggsData = ( const isAllZeros = data.every(({ y }) => y === 0); - const warnings = constructWarnings(timestampNotFound, totalCount, range); - return { data, totalCount: isAllZeros ? 0 : totalCount, - lte: from, - gte: to, inspect: { dsl, response: inspectResponse, }, - warnings, + refetch, }; }; @@ -151,19 +123,15 @@ export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => export const getSequenceAggs = ( response: EqlSearchStrategyResponse>, - range: Unit, - to: string, - from: string + refetch: inputsModel.Refetch ): EqlPreviewResponse => { const { dsl, response: inspectResponse } = formatInspect(response); const sequences = response.rawResponse.body.hits.sequences ?? []; const totalCount = response.rawResponse.body.hits.total.value; - let timestampNotFound = false; const data = sequences.map((sequence, i) => { return sequence.events.map((seqEvent) => { if (seqEvent._source['@timestamp'] == null) { - timestampNotFound = true; return {}; } return { @@ -174,17 +142,13 @@ export const getSequenceAggs = ( }); }); - const warnings = constructWarnings(timestampNotFound, totalCount, range); - return { data: data.flat(), totalCount, - lte: from, - gte: to, inspect: { dsl, response: inspectResponse, }, - warnings, + refetch, }; }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts index e7ccf83591d81..5bd51da28badc 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts @@ -3,16 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Unit } from '@elastic/datemath'; + import { InspectResponse } from '../../../types'; import { ChartData } from '../../components/charts/common'; +import { inputsModel } from '../../../common/store'; + +export interface EqlPreviewRequest { + to: string; + from: string; + interval: Unit; + query: string; + index: string[]; +} export interface EqlPreviewResponse { data: ChartData[]; totalCount: number; - lte: string; - gte: string; inspect: InspectResponse; - warnings: string[]; + refetch: inputsModel.Refetch; } export interface Source { diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts new file mode 100644 index 0000000000000..ae7a263cc7012 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Unit } from '@elastic/datemath'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { of, throwError } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import * as i18n from '../translations'; +import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; +import { Source } from './types'; +import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { useEqlPreview } from '.'; +import { getMockResponse } from './helpers.test'; + +jest.mock('../../../common/lib/kibana'); + +describe('useEqlPreview', () => { + const params = { + to: '2020-10-04T16:00:54.368707900Z', + query: 'file where true', + index: ['foo-*', 'bar-*'], + interval: 'h' as Unit, + from: '2020-10-04T15:00:54.368707900Z', + }; + + beforeEach(() => { + useKibana().services.notifications.toasts.addError = jest.fn(); + + useKibana().services.notifications.toasts.addWarning = jest.fn(); + + (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse())); + }); + + it('should initiate hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + await waitForNextUpdate(); + + expect(result.current[0]).toBeFalsy(); + expect(typeof result.current[1]).toEqual('function'); + expect(result.current[2]).toEqual({ + data: [], + inspect: { dsl: [], response: [] }, + refetch: result.current[2].refetch, + totalCount: 0, + }); + }); + }); + + it('should invoke search with passed in params', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(mockCalls[0][0].params.body.query).toEqual('file where true'); + expect(mockCalls[0][0].params.body.filter).toEqual({ + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-10-04T15:00:54.368707900Z', + lte: '2020-10-04T16:00:54.368707900Z', + }, + }, + }); + expect(mockCalls[0][0].params.index).toBe('foo-*,bar-*'); + }); + }); + + it('should resolve values after search is invoked', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + expect(result.current[0]).toBeFalsy(); + expect(typeof result.current[1]).toEqual('function'); + expect(result.current[2].totalCount).toEqual(4); + expect(result.current[2].data.length).toBeGreaterThan(0); + expect(result.current[2].inspect.dsl.length).toBeGreaterThan(0); + expect(result.current[2].inspect.response.length).toBeGreaterThan(0); + }); + }); + + it('should not resolve values after search is invoked if component unmounted', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + of(getMockResponse()).pipe(delay(5000)) + ); + const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + unmount(); + + expect(result.current[0]).toBeTruthy(); + expect(result.current[2].totalCount).toEqual(0); + expect(result.current[2].data.length).toEqual(0); + expect(result.current[2].inspect.dsl.length).toEqual(0); + expect(result.current[2].inspect.response.length).toEqual(0); + }); + }); + + it('should not resolve new values on search if response is error response', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + of({ isRunning: false, isPartial: true } as EqlSearchStrategyResponse< + EqlSearchResponse + >) + ); + + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.notifications.toasts.addWarning as jest.Mock).mock + .calls; + + expect(result.current[0]).toBeFalsy(); + expect(mockCalls[0][0]).toEqual(i18n.EQL_PREVIEW_FETCH_FAILURE); + }); + }); + + it('should add danger toast if search throws', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + throwError('This is an error!') + ); + + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.notifications.toasts.addError as jest.Mock).mock + .calls; + + expect(result.current[0]).toBeFalsy(); + expect(mockCalls[0][0]).toEqual('This is an error!'); + }); + }); + + it('returns a memoized value', async () => { + const { result, rerender } = renderHook(() => useEqlPreview()); + + const result1 = result.current[1]; + act(() => rerender()); + const result2 = result.current[1]; + + expect(result1).toBe(result2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts index 384395b34e62b..1bfaecdf089be 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts @@ -3,10 +3,157 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { noop } from 'lodash/fp'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; -import { useAsync, withOptionalSignal } from '../../../shared_imports'; -import { getEqlPreview } from './api'; +import * as i18n from '../translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; +import { + EqlSearchStrategyRequest, + EqlSearchStrategyResponse, +} from '../../../../../data_enhanced/common'; +import { getEqlAggsData, getSequenceAggs } from './helpers'; +import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types'; +import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; +import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; +import { inputsModel } from '../../../common/store'; +import { EQL_SEARCH_STRATEGY } from '../../../../../data_enhanced/public'; -const getEqlPreviewWithOptionalSignal = withOptionalSignal(getEqlPreview); +export const useEqlPreview = (): [ + boolean, + (arg: EqlPreviewRequest) => void, + EqlPreviewResponse +] => { + const { data, notifications } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const unsubscribeStream = useRef(new Subject()); + const [loading, setLoading] = useState(false); + const didCancel = useRef(false); -export const useEqlPreview = () => useAsync(getEqlPreviewWithOptionalSignal); + const [response, setResponse] = useState({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: 0, + }); + + const searchEql = useCallback( + ({ from, to, query, index, interval }: EqlPreviewRequest) => { + if (parseScheduleDates(to) == null || parseScheduleDates(from) == null) { + notifications.toasts.addWarning('Time intervals are not defined.'); + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + setResponse((prevResponse) => ({ + ...prevResponse, + data: [], + inspect: { + dsl: [], + response: [], + }, + totalCount: 0, + })); + + data.search + .search>>( + { + params: { + // @ts-expect-error allow_no_indices is missing on EqlSearch + allow_no_indices: true, + index: index.join(), + body: { + filter: { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + query, + // EQL requires a cap, otherwise it defaults to 10 + // It also sorts on ascending order, capping it at + // something smaller like 20, made it so that some of + // the more recent events weren't returned + size: 100, + }, + }, + }, + { + strategy: EQL_SEARCH_STRATEGY, + abortSignal: abortCtrl.current.signal, + } + ) + .pipe(takeUntil(unsubscribeStream.current)) + .subscribe({ + next: (res) => { + if (isCompleteResponse(res)) { + if (!didCancel.current) { + setLoading(false); + if (hasEqlSequenceQuery(query)) { + setResponse(getSequenceAggs(res, refetch.current)); + } else { + setResponse(getEqlAggsData(res, interval, to, refetch.current)); + } + } + unsubscribeStream.current.next(); + } else if (isErrorResponse(res)) { + setLoading(false); + notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE); + unsubscribeStream.current.next(); + } + }, + error: (err) => { + if (!(err instanceof AbortError)) { + setLoading(false); + setResponse({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: 0, + }); + notifications.toasts.addError(err, { + title: i18n.EQL_PREVIEW_FETCH_FAILURE, + }); + } + }, + }); + }; + + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, notifications.toasts] + ); + + useEffect((): (() => void) => { + return (): void => { + didCancel.current = true; + abortCtrl.current.abort(); + // eslint-disable-next-line react-hooks/exhaustive-deps + unsubscribeStream.current.complete(); + }; + }, []); + + return [loading, searchEql, response]; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/hooks/translations.ts index 50aeb76686962..2c6300046b7bd 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/translations.ts @@ -32,3 +32,10 @@ export const INDEX_PATTERN_FETCH_FAILURE = i18n.translate( defaultMessage: 'Index pattern fetch failure', } ); + +export const EQL_PREVIEW_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.components.hooks.eql.partialResponse', + { + defaultMessage: 'EQL Preview Error', + } +); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts new file mode 100644 index 0000000000000..51f05984aa837 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createDefaultIndexPatterns, Args } from './helpers'; +import { initialSourcererState, SourcererScopeName } from './model'; + +let defaultArgs: Args = { + eventType: 'all', + id: SourcererScopeName.default, + selectedPatterns: ['auditbeat-*', 'packetbeat-*'], + state: { + ...initialSourcererState, + configIndexPatterns: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'], + kibanaIndexPatterns: [{ id: '123', title: 'journalbeat-*' }], + signalIndexName: 'signals-*', + }, +}; +const eventTypes: Array = ['all', 'raw', 'alert', 'signal', 'custom']; +const ids: Array = [ + SourcererScopeName.default, + SourcererScopeName.detections, + SourcererScopeName.timeline, +]; +describe('createDefaultIndexPatterns', () => { + ids.forEach((id) => { + eventTypes.forEach((et) => { + describe(`id: ${id}, eventType: ${et}`, () => { + beforeEach(() => { + defaultArgs = { + ...defaultArgs, + id, + eventType: et, + }; + }); + it('Selected patterns', () => { + const result = createDefaultIndexPatterns(defaultArgs); + expect(result).toEqual(['auditbeat-*', 'packetbeat-*']); + }); + it('No selected patterns', () => { + const newArgs = { + ...defaultArgs, + selectedPatterns: [], + }; + const result = createDefaultIndexPatterns(newArgs); + if ( + id === SourcererScopeName.detections || + (id === SourcererScopeName.timeline && (et === 'alert' || et === 'signal')) + ) { + expect(result).toEqual(['signals-*']); + } else if (id === SourcererScopeName.timeline && et === 'all') { + expect(result).toEqual(['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'signals-*']); + } else { + expect(result).toEqual(['filebeat-*', 'auditbeat-*', 'packetbeat-*']); + } + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts new file mode 100644 index 0000000000000..3ae9740cfd51d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// eslint-disable-next-line no-restricted-imports +import isEmpty from 'lodash/isEmpty'; +import { SourcererModel, SourcererScopeName } from './model'; +import { TimelineEventsType } from '../../../../common/types/timeline'; + +export interface Args { + eventType?: TimelineEventsType; + id: SourcererScopeName; + selectedPatterns: string[]; + state: SourcererModel; +} +export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, state }: Args) => { + const kibanaIndexPatterns = state.kibanaIndexPatterns.map((kip) => kip.title); + const newSelectedPatterns = selectedPatterns.filter( + (sp) => + state.configIndexPatterns.includes(sp) || + kibanaIndexPatterns.includes(sp) || + (!isEmpty(state.signalIndexName) && state.signalIndexName === sp) + ); + if (isEmpty(newSelectedPatterns)) { + let defaultIndexPatterns = state.configIndexPatterns; + if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) { + if (eventType === 'all' && !isEmpty(state.signalIndexName)) { + defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? '']; + } else if (eventType === 'raw') { + defaultIndexPatterns = state.configIndexPatterns; + } else if ( + !isEmpty(state.signalIndexName) && + (eventType === 'signal' || eventType === 'alert') + ) { + defaultIndexPatterns = [state.signalIndexName ?? '']; + } + } else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) { + defaultIndexPatterns = [state.signalIndexName ?? '']; + } + return defaultIndexPatterns; + } + return newSelectedPatterns; +}; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts index 221244aaf9200..a1112607de24f 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -5,8 +5,7 @@ */ // Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEmpty from 'lodash/isEmpty'; + import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { @@ -16,7 +15,8 @@ import { setSignalIndexName, setSource, } from './actions'; -import { initialSourcererState, SourcererModel, SourcererScopeName } from './model'; +import { initialSourcererState, SourcererModel } from './model'; +import { createDefaultIndexPatterns } from './helpers'; export type SourcererState = SourcererModel; @@ -41,37 +41,13 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState) }, })) .case(setSelectedIndexPatterns, (state, { id, selectedPatterns, eventType }) => { - const kibanaIndexPatterns = state.kibanaIndexPatterns.map((kip) => kip.title); - const newSelectedPatterns = selectedPatterns.filter( - (sp) => - state.configIndexPatterns.includes(sp) || - kibanaIndexPatterns.includes(sp) || - (!isEmpty(state.signalIndexName) && state.signalIndexName === sp) - ); - let defaultIndexPatterns = state.configIndexPatterns; - if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) { - if (eventType === 'all' && !isEmpty(state.signalIndexName)) { - defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? '']; - } else if (eventType === 'raw') { - defaultIndexPatterns = state.configIndexPatterns; - } else if ( - !isEmpty(state.signalIndexName) && - (eventType === 'signal' || eventType === 'alert') - ) { - defaultIndexPatterns = [state.signalIndexName ?? '']; - } - } else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) { - defaultIndexPatterns = [state.signalIndexName ?? '']; - } return { ...state, sourcererScopes: { ...state.sourcererScopes, [id]: { ...state.sourcererScopes[id], - selectedPatterns: isEmpty(newSelectedPatterns) - ? defaultIndexPatterns - : newSelectedPatterns, + selectedPatterns: createDefaultIndexPatterns({ eventType, id, selectedPatterns, state }), }, }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 533f13e6781a6..9925dfd4c062f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -5,9 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { shallow, mount } from 'enzyme'; import '../../../common/mock/match_media'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { TestProviders } from '../../../common/mock'; import { AlertsHistogramPanel } from './index'; jest.mock('react-router-dom', () => { @@ -31,12 +34,16 @@ jest.mock('../../../common/lib/kibana', () => { navigateToApp: mockNavigateToApp, getUrlForApp: jest.fn(), }, + uiSettings: { + get: jest.fn(), + }, }, }), useUiSetting$: jest.fn().mockReturnValue([]), useGetUserSavedObjectPermissions: jest.fn(), }; }); + jest.mock('../../../common/components/navigation/use_get_url_search'); describe('AlertsHistogramPanel', () => { @@ -77,4 +84,23 @@ describe('AlertsHistogramPanel', () => { expect(mockNavigateToApp).toBeCalledWith('securitySolution:detections', { path: '' }); }); }); + + describe('Query', () => { + it('it render with a illegal KQL', async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 3bc84bb7c32ee..c96ef570c7e09 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -221,24 +221,28 @@ export const AlertsHistogramPanel = memo( }, [alertsData]); useEffect(() => { - const converted = esQuery.buildEsQuery( - undefined, - query != null ? [query] : [], - filters?.filter((f) => f.meta.disabled === false) ?? [], - { - ...esQuery.getEsQueryConfig(kibana.services.uiSettings), - dateFormatTZ: undefined, - } - ); + try { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter((f) => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); - setAlertsQuery( - getAlertsHistogramQuery( - selectedStackByOption.value, - from, - to, - !isEmpty(converted) ? [converted] : [] - ) - ); + setAlertsQuery( + getAlertsHistogramQuery( + selectedStackByOption.value, + from, + to, + !isEmpty(converted) ? [converted] : [] + ) + ); + } catch (e) { + setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, [])); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedStackByOption.value, from, to, query, filters]); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 2ce9d1ea68b3c..ebdfdcc262b34 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -168,8 +168,11 @@ describe('helpers', () => { query: mockQueryBarWithQuery.query, savedId: mockQueryBarWithQuery.saved_id, }); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL}); + expect(shallow(result[0].description as React.ReactElement).text()).toEqual( + mockQueryBarWithQuery.query + ); }); test('returns expected array of ListItems when "savedId" exists', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 9ef1dd2bcb204..83413496c609d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -51,6 +51,10 @@ const EuiBadgeWrap = (styled(EuiBadge)` } ` as unknown) as typeof EuiBadge; +const Query = styled.div` + white-space: pre-wrap; +`; + export const buildQueryBarDescription = ({ field, filters, @@ -92,8 +96,8 @@ export const buildQueryBarDescription = ({ items = [ ...items, { - title: <>{queryLabel ?? i18n.QUERY_LABEL} , - description: <>{query} , + title: <>{queryLabel ?? i18n.QUERY_LABEL}, + description: {query}, }, ]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 8179e5865e4ef..d881d05edbb0c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -315,8 +315,10 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL}); + expect(shallow(result[0].description as React.ReactElement).text()).toEqual( + mockQueryBar.queryBar.query.query + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx index f7ee5be18154c..1d57ef2bb2cda 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx @@ -27,15 +27,30 @@ export interface EqlQueryBarProps { dataTestSubj: string; field: FieldHook; idAria?: string; + onValidityChange?: (arg: boolean) => void; } -export const EqlQueryBar: FC = ({ dataTestSubj, field, idAria }) => { +export const EqlQueryBar: FC = ({ + dataTestSubj, + field, + idAria, + onValidityChange, +}) => { const { addError } = useAppToasts(); const [errorMessages, setErrorMessages] = useState([]); - const { setValue } = field; + const { isValidating, setValue } = field; const { isValid, message, messages, error } = getValidationResults(field); const fieldValue = field.value.query.query as string; + // Bubbles up field validity to parent. + // Using something like form `getErrors` does + // not guarantee latest validity state + useEffect(() => { + if (onValidityChange != null) { + onValidityChange(isValid); + } + }, [isValid, onValidityChange]); + useEffect(() => { setErrorMessages(messages ?? []); }, [messages]); @@ -81,7 +96,7 @@ export const EqlQueryBar: FC = ({ dataTestSubj, field, idAria value={fieldValue} onChange={handleChange} /> - + ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx index 19bab26f8aa58..7c0ddd6d8b3cc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; import * as i18n from './translations'; import { ErrorsPopover } from './errors_popover'; @@ -14,25 +14,31 @@ import { EqlOverviewLink } from './eql_overview_link'; export interface Props { errors: string[]; + isLoading?: boolean; } const Container = styled(EuiPanel)` border-radius: 0; background: ${({ theme }) => theme.eui.euiPageBackgroundColor}; - padding: ${({ theme }) => theme.eui.euiSizeXS}; + padding: ${({ theme }) => theme.eui.euiSizeXS} ${({ theme }) => theme.eui.euiSizeS}; `; const FlexGroup = styled(EuiFlexGroup)` min-height: ${({ theme }) => theme.eui.euiSizeXL}; `; -export const EqlQueryBarFooter: FC = ({ errors }) => ( +const Spinner = styled(EuiLoadingSpinner)` + margin: 0 ${({ theme }) => theme.eui.euiSizeS}; +`; + +export const EqlQueryBarFooter: FC = ({ errors, isLoading }) => ( {errors.length > 0 && ( )} + {isLoading && } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index c53a9ccc22d8b..4cb2abe756cf3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -46,6 +46,7 @@ interface QueryBarDefineRuleProps { onCloseTimelineSearch: () => void; openTimelineSearch: boolean; resizeParentContainer?: (height: number) => void; + onValidityChange?: (arg: boolean) => void; } const StyledEuiFormRow = styled(EuiFormRow)` @@ -74,6 +75,7 @@ export const QueryBarDefineRule = ({ onCloseTimelineSearch, openTimelineSearch = false, resizeParentContainer, + onValidityChange, }: QueryBarDefineRuleProps) => { const [originalHeight, setOriginalHeight] = useState(-1); const [loadingTimeline, setLoadingTimeline] = useState(false); @@ -86,6 +88,15 @@ export const QueryBarDefineRule = ({ const savedQueryServices = useSavedQueryServices(); + // Bubbles up field validity to parent. + // Using something like form `getErrors` does + // not guarantee latest validity state + useEffect((): void => { + if (onValidityChange != null) { + onValidityChange(!isInvalid); + } + }, [isInvalid, onValidityChange]); + useEffect(() => { let isSubscribed = true; const subscriptions = new Subscription(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx new file mode 100644 index 0000000000000..01d95fa80ba59 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewCustomQueryHistogram } from './custom_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewCustomQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING); + }); + + test('it configures data and subtitle', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).props().data + ).toEqual([ + { + key: 'hits', + value: [ + { + g: 'All others', + x: 1602247050000, + y: 2314, + }, + { + g: 'All others', + x: 1602247162500, + y: 3471, + }, + { + g: 'All others', + x: 1602247275000, + y: 3369, + }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryPreviewCustomHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx new file mode 100644 index 0000000000000..787e8dab393ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useMemo } from 'react'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { getHistogramConfig } from './helpers'; +import { + ChartSeriesConfigs, + ChartSeriesData, + ChartData, +} from '../../../../common/components/charts/common'; +import { InspectResponse } from '../../../../../public/types'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; + +export const ID = 'queryPreviewCustomHistogramQuery'; + +interface PreviewCustomQueryHistogramProps { + to: string; + from: string; + isLoading: boolean; + data: ChartData[]; + totalCount: number; + inspect: InspectResponse; + refetch: inputsModel.Refetch; +} + +export const PreviewCustomQueryHistogram = ({ + to, + from, + data, + totalCount, + inspect, + refetch, + isLoading, +}: PreviewCustomQueryHistogramProps) => { + const { setQuery, isInitializing } = useGlobalTime(); + + useEffect((): void => { + if (!isLoading && !isInitializing) { + setQuery({ id: ID, inspect, loading: isLoading, refetch }); + } + }, [setQuery, inspect, isLoading, isInitializing, refetch]); + + const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from, true), [ + from, + to, + ]); + + const subtitle = useMemo( + (): string => + isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), + [isLoading, totalCount] + ); + + const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx new file mode 100644 index 0000000000000..16e71485de9a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewEqlQueryHistogram } from './eql_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewEqlQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING); + }); + + test('it configures data and subtitle', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ + { + key: 'hits', + value: [ + { + g: 'All others', + x: 1602247050000, + y: 2314, + }, + { + g: 'All others', + x: 1602247162500, + y: 3471, + }, + { + g: 'All others', + x: 1602247275000, + y: 3369, + }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryEqlPreviewHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx index 3211afea821b0..8f2774a1342b6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx @@ -5,74 +5,74 @@ */ import React, { useEffect, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import * as i18n from './translations'; -import { BarChart } from '../../../../common/components/charts/barchart'; import { getHistogramConfig } from './helpers'; -import { ChartData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; +import { + ChartSeriesData, + ChartSeriesConfigs, + ChartData, +} from '../../../../common/components/charts/common'; import { InspectQuery } from '../../../../common/store/inputs/model'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; +import { hasEqlSequenceQuery } from '../../../../../common/detection_engine/utils'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; export const ID = 'queryEqlPreviewHistogramQuery'; interface PreviewEqlQueryHistogramProps { to: string; from: string; - totalHits: number; + totalCount: number; + isLoading: boolean; + query: string; data: ChartData[]; inspect: InspectQuery; + refetch: inputsModel.Refetch; } export const PreviewEqlQueryHistogram = ({ from, to, - totalHits, + totalCount, + query, data, inspect, + refetch, + isLoading, }: PreviewEqlQueryHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); useEffect((): void => { if (!isInitializing) { - setQuery({ id: ID, inspect, loading: false, refetch: () => {} }); + setQuery({ id: ID, inspect, loading: false, refetch }); } - }, [setQuery, inspect, isInitializing]); + }, [setQuery, inspect, isInitializing, refetch]); - const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]); + const barConfig = useMemo( + (): ChartSeriesConfigs => getHistogramConfig(to, from, hasEqlSequenceQuery(query)), + [from, to, query] + ); + + const subtitle = useMemo( + (): string => + isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), + [isLoading, totalCount] + ); + + const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); return ( - <> - - - - - - - - - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER_EQL}

-
- -
-
-
- + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts new file mode 100644 index 0000000000000..41ac95338460f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isNoisy, getTimeframeOptions, getInfoFromQueryBar } from './helpers'; + +describe('query_preview/helpers', () => { + describe('isNoisy', () => { + test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(2, 'h'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last hour" and average hits per hour is one', () => { + const isItNoisy = isNoisy(1, 'h'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last hour" and hits is 0', () => { + const isItNoisy = isNoisy(1, 'h'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(50, 'd'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last day" and average hits per hour is one', () => { + const isItNoisy = isNoisy(24, 'd'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last day" and hits is 0', () => { + const isItNoisy = isNoisy(0, 'd'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(1000, 'M'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last month" and average hits per hour is one', () => { + const isItNoisy = isNoisy(730, 'M'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last month" and hits is 0', () => { + const isItNoisy = isNoisy(1, 'M'); + + expect(isItNoisy).toBeFalsy(); + }); + }); + + describe('getTimeframeOptions', () => { + test('returns hour and day options if ruleType is eql', () => { + const options = getTimeframeOptions('eql'); + + expect(options).toEqual([ + { value: 'h', text: 'Last hour' }, + { value: 'd', text: 'Last day' }, + ]); + }); + + test('returns hour, day, and month options if ruleType is not eql', () => { + const options = getTimeframeOptions('query'); + + expect(options).toEqual([ + { value: 'h', text: 'Last hour' }, + { value: 'd', text: 'Last day' }, + { value: 'M', text: 'Last month' }, + ]); + }); + }); + + describe('getInfoFromQueryBar', () => { + test('returns queryFilter when ruleType is query', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'query' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns queryFilter when ruleType is saved_query', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'saved_query' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns queryFilter when ruleType is threshold', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'threshold' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns undefined queryFilter when ruleType is eql', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'file where true', language: 'eql' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'eql' + ); + + expect(queryString).toEqual('file where true'); + expect(language).toEqual('eql'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toBeUndefined(); + }); + + test('returns undefined queryFilter when getQueryFilter throws', () => { + // query is malformed, forcing error in getQueryFilter + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'threshold' + ); + + expect(queryString).toEqual('host.name:'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts index 4cf37236510d7..ed8994a4c44fd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts @@ -5,11 +5,16 @@ */ import { Position, ScaleType } from '@elastic/charts'; import { EuiSelectOption } from '@elastic/eui'; +import { Unit } from '@elastic/datemath'; import * as i18n from './translations'; import { histogramDateTimeFormatter } from '../../../../common/components/utils'; import { ChartSeriesConfigs } from '../../../../common/components/charts/common'; -import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Type, Language } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { FieldValueQueryBar } from '../query_bar'; +import { ESQuery } from '../../../../../common/typed_json'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; export const HITS_THRESHOLD: Record = { h: 1, @@ -17,6 +22,18 @@ export const HITS_THRESHOLD: Record = { M: 730, }; +export const isNoisy = (hits: number, timeframe: Unit) => { + if (timeframe === 'h') { + return hits > 1; + } else if (timeframe === 'd') { + return hits / 24 > 1; + } else if (timeframe === 'M') { + return hits / 730 > 1; + } + + return false; +}; + export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => { if (ruleType === 'eql') { return [ @@ -32,7 +49,50 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => { } }; -export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs => { +export const getInfoFromQueryBar = ( + queryBar: FieldValueQueryBar, + index: string[], + ruleType: Type +): { + queryString: string; + language: Language; + filters: Filter[]; + queryFilter: ESQuery | undefined; +} => { + const queryString = typeof queryBar.query.query === 'string' ? queryBar.query.query : ''; + const language = queryBar.query.language as Language; + const filters = queryBar.filters; + + // hm?? Why a try catch here? Because if the + // query is invalid, it throws an error and + // entire UI shows gross KQLSyntax error screen + try { + const queryFilter = + ruleType !== 'eql' + ? getQueryFilter(queryString, language, filters, index, [], true) + : undefined; + + return { + queryString, + language, + filters, + queryFilter, + }; + } catch { + return { + queryString, + language, + filters, + queryFilter: undefined, + }; + } +}; + +export const getHistogramConfig = ( + to: string, + from: string, + showLegend: boolean = false +): ChartSeriesConfigs => { return { series: { xScaleType: ScaleType.Time, @@ -47,8 +107,8 @@ export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs yAxisTitle: i18n.QUERY_GRAPH_COUNT, settings: { legendPosition: Position.Right, - showLegend: true, - showLegendExtra: true, + showLegend, + showLegendExtra: showLegend, theme: { scales: { barsPadding: 0.08, @@ -74,11 +134,12 @@ export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs export const getThresholdHistogramConfig = (height: number | undefined): ChartSeriesConfigs => { return { series: { - xScaleType: ScaleType.Linear, + xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, stackAccessors: ['g'], }, axis: { + yTickFormatter: (value: string | number): string => value.toLocaleString(), tickSize: 8, }, yAxisTitle: i18n.QUERY_GRAPH_COUNT, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx new file mode 100644 index 0000000000000..d6ccd16083023 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TestProviders } from '../../../../common/mock'; +import { PreviewHistogram } from './histogram'; +import { getHistogramConfig } from './helpers'; + +describe('PreviewHistogram', () => { + test('it renders loading icon if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders chart if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx new file mode 100644 index 0000000000000..2c43dac7b6bce --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BarChart } from '../../../../common/components/charts/barchart'; +import { Panel } from '../../../../common/components/panel'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; + +const LoadingChart = styled(EuiLoadingChart)` + display: block; + margin: 0 auto; +`; + +interface PreviewHistogramProps { + id: string; + data: ChartSeriesData[]; + barConfig: ChartSeriesConfigs; + title: string; + subtitle: string; + disclaimer: string; + isLoading: boolean; +} + +export const PreviewHistogram = ({ + id, + data, + barConfig, + title, + subtitle, + disclaimer, + isLoading, +}: PreviewHistogramProps) => { + return ( + <> + + + + + + + {isLoading ? ( + + ) : ( + + )} + + + <> + + +

{disclaimer}

+
+ +
+
+
+ + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx new file mode 100644 index 0000000000000..87436ad1e6d2b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -0,0 +1,258 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { of } from 'rxjs'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TestProviders } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import { PreviewQuery } from './'; +import { getMockResponse } from '../../../../common/hooks/eql/helpers.test'; + +jest.mock('../../../../common/lib/kibana'); + +describe('PreviewQuery', () => { + beforeEach(() => { + useKibana().services.notifications.toasts.addError = jest.fn(); + + useKibana().services.notifications.toasts.addWarning = jest.fn(); + + (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse())); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders timeframe select and preview button on render', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders preview button disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeTruthy(); + }); + + test('it renders preview button disabled if "query" is undefined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeTruthy(); + }); + + test('it renders query histogram when rule type is query and preview button clicked', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when rule type is saved_query and preview button clicked', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders eql histogram when preview button clicked and rule type is eql', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy(); + }); + + test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 43dcdb7b7d58d..f1cb8e3ba9fdb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { Fragment, useCallback, useEffect, useReducer } from 'react'; import { Unit } from '@elastic/datemath'; -import { getOr } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, @@ -14,25 +13,23 @@ import { EuiFormRow, EuiButton, EuiCallOut, - EuiSelectOption, EuiText, EuiSpacer, } from '@elastic/eui'; +import { debounce } from 'lodash/fp'; import * as i18n from './translations'; -import { useKibana } from '../../../../common/lib/kibana'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; -import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; +import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; import { FieldValueQueryBar } from '../query_bar'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { Language, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { PreviewEqlQueryHistogram } from './eql_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { PreviewNonEqlQueryHistogram } from './non_eql_histogram'; -import { getTimeframeOptions } from './helpers'; +import { useEqlPreview } from '../../../../common/hooks/eql/'; import { PreviewThresholdQueryHistogram } from './threshold_histogram'; import { formatDate } from '../../../../common/components/super_date_picker'; +import { State, queryPreviewReducer } from './reducer'; +import { isNoisy } from './helpers'; +import { PreviewCustomQueryHistogram } from './custom_histogram'; const Select = styled(EuiSelect)` width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; @@ -42,13 +39,30 @@ const PreviewButton = styled(EuiButton)` margin-left: 0; `; +export const initialState: State = { + timeframeOptions: [], + showHistogram: false, + timeframe: 'h', + warnings: [], + queryFilter: undefined, + toTime: '', + fromTime: '', + queryString: '', + language: 'kuery', + filters: [], + thresholdFieldExists: false, + showNonEqlHistogram: false, +}; + +export type Threshold = { field: string | undefined; value: number } | undefined; + interface PreviewQueryProps { dataTestSubj: string; idAria: string; query: FieldValueQueryBar | undefined; index: string[]; ruleType: Type; - threshold: { field: string | undefined; value: number } | undefined; + threshold: Threshold; isDisabled: boolean; } @@ -61,131 +75,176 @@ export const PreviewQuery = ({ threshold, isDisabled, }: PreviewQueryProps) => { - const { data } = useKibana().services; - const { addError } = useAppToasts(); + const [ + eqlQueryLoading, + startEql, + { + totalCount: eqlQueryTotal, + data: eqlQueryData, + refetch: eqlQueryRefetch, + inspect: eqlQueryInspect, + }, + ] = useEqlPreview(); - const [timeframeOptions, setTimeframeOptions] = useState([]); - const [showHistogram, setShowHistogram] = useState(false); - const [timeframe, setTimeframe] = useState('h'); - const [warnings, setWarnings] = useState([]); - const [queryFilter, setQueryFilter] = useState(undefined); - const [toTime, setTo] = useState(''); - const [fromTime, setFrom] = useState(''); - const { - error: eqlError, - start: startEql, - result: eqlQueryResult, - loading: eqlQueryLoading, - } = useEqlPreview(); + const [ + { + thresholdFieldExists, + showNonEqlHistogram, + timeframeOptions, + showHistogram, + timeframe, + warnings, + queryFilter, + toTime, + fromTime, + queryString, + }, + dispatch, + ] = useReducer(queryPreviewReducer(), { + ...initialState, + toTime: formatDate('now-1h'), + fromTime: formatDate('now'), + }); + const [ + isMatrixHistogramLoading, + { inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets }, + startNonEql, + ] = useMatrixHistogram({ + errorMessage: i18n.PREVIEW_QUERY_ERROR, + endDate: fromTime, + startDate: toTime, + filterQuery: queryFilter, + indexNames: index, + histogramType: MatrixHistogramType.events, + stackByField: 'event.category', + threshold, + skip: true, + }); - const queryString = useMemo((): string => getOr('', 'query.query', query), [query]); - const language = useMemo((): Language => getOr('kuery', 'query.language', query), [query]); - const filters = useMemo((): Filter[] => (query != null ? query.filters : []), [query]); + const setQueryInfo = useCallback( + (queryBar: FieldValueQueryBar | undefined): void => { + dispatch({ + type: 'setQueryInfo', + queryBar, + index, + ruleType, + }); + }, + [dispatch, index, ruleType] + ); - const handleCalculateTimeRange = useCallback((): void => { - const from = formatDate('now'); - const to = formatDate(`now-1${timeframe}`); + const setTimeframeSelect = useCallback( + (selection: Unit): void => { + dispatch({ + type: 'setTimeframeSelect', + timeframe: selection, + }); + }, + [dispatch] + ); - setTo(to); - setFrom(from); - }, [timeframe]); + const setRuleTypeChange = useCallback( + (type: Type): void => { + dispatch({ + type: 'setResetRuleTypeChange', + ruleType: type, + }); + }, + [dispatch] + ); - const handlePreviewEqlQuery = useCallback((): void => { - startEql({ - data, - index, - query: queryString, - fromTime, - toTime, - interval: timeframe, - }); - }, [startEql, data, index, queryString, fromTime, toTime, timeframe]); + const setWarnings = useCallback( + (yikes: string[]): void => { + dispatch({ + type: 'setWarnings', + warnings: yikes, + }); + }, + [dispatch] + ); - const handleSelectPreviewTimeframe = ({ - target: { value }, - }: React.ChangeEvent): void => { - setTimeframe(value as Unit); - setShowHistogram(false); - }; + const setNoiseWarning = useCallback((): void => { + dispatch({ + type: 'setNoiseWarning', + }); + }, [dispatch]); - const handlePreviewClicked = useCallback((): void => { - handleCalculateTimeRange(); + const setShowHistogram = useCallback( + (show: boolean): void => { + dispatch({ + type: 'setShowHistogram', + show, + }); + }, + [dispatch] + ); - if (ruleType === 'eql') { - setShowHistogram(true); - handlePreviewEqlQuery(); - } else { - const builtFilterQuery = { - ...((getQueryFilter( - queryString, - language, - filters, - index, - [], - true - ) as unknown) as ESQueryStringQuery), - }; - if (builtFilterQuery != null) { - setShowHistogram(true); - } - setQueryFilter(builtFilterQuery); - } - }, [ - filters, - handleCalculateTimeRange, - handlePreviewEqlQuery, - index, - language, - queryString, - ruleType, - ]); + const setThresholdValues = useCallback( + (thresh: Threshold, type: Type): void => { + dispatch({ + type: 'setThresholdQueryVals', + threshold: thresh, + ruleType: type, + }); + }, + [dispatch] + ); useEffect((): void => { - if (eqlError != null) { - addError(eqlError, { title: i18n.PREVIEW_QUERY_ERROR }); - } - }, [eqlError, addError]); + const debounced = debounce(1000, setQueryInfo); - // reset when rule type changes - useEffect((): void => { - const options = getTimeframeOptions(ruleType); + debounced(query); + }, [setQueryInfo, query]); - setShowHistogram(false); - setTimeframe('h'); - setTimeframeOptions(options); - setWarnings([]); - }, [ruleType]); + useEffect((): void => { + setThresholdValues(threshold, ruleType); + }, [setThresholdValues, threshold, ruleType]); - // reset when timeframe or query changes useEffect((): void => { - setShowHistogram(false); - setWarnings([]); - }, [timeframe, queryString]); + setRuleTypeChange(ruleType); + }, [ruleType, setRuleTypeChange]); useEffect((): void => { - if (eqlQueryResult != null) { - setWarnings((prevWarnings) => { - if (eqlQueryResult.warnings.join() !== prevWarnings.join()) { - return eqlQueryResult.warnings; - } + const totalHits = ruleType === 'eql' ? eqlQueryTotal : matrixHistTotal; - return prevWarnings; - }); + if (isNoisy(totalHits, timeframe)) { + setNoiseWarning(); } - }, [eqlQueryResult]); + }, [timeframe, matrixHistTotal, eqlQueryTotal, ruleType, setNoiseWarning]); + + const handlePreviewEqlQuery = useCallback( + (to: string, from: string): void => { + startEql({ + index, + query: queryString, + from, + to, + interval: timeframe, + }); + }, + [startEql, index, queryString, timeframe] + ); - const thresholdFieldExists = useMemo( - (): boolean => threshold != null && threshold.field != null && threshold.field.trim() !== '', - [threshold] + const handleSelectPreviewTimeframe = useCallback( + ({ target: { value } }: React.ChangeEvent): void => { + setTimeframeSelect(value as Unit); + }, + [setTimeframeSelect] ); - const showNonEqlHistogram = useMemo((): boolean => { - return ( - ruleType === 'query' || - ruleType === 'saved_query' || - (ruleType === 'threshold' && !thresholdFieldExists) - ); - }, [ruleType, thresholdFieldExists]); + const handlePreviewClicked = useCallback((): void => { + const to = formatDate('now'); + const from = formatDate(`now-1${timeframe}`); + + setWarnings([]); + setShowHistogram(true); + + if (ruleType === 'eql') { + handlePreviewEqlQuery(to, from); + } else { + startNonEql(to, from); + } + }, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]); return ( <> @@ -209,7 +268,14 @@ export const PreviewQuery = ({ />
- + {i18n.PREVIEW_LABEL} @@ -217,41 +283,50 @@ export const PreviewQuery = ({ {showNonEqlHistogram && showHistogram && ( - )} {ruleType === 'threshold' && thresholdFieldExists && showHistogram && ( )} - {ruleType === 'eql' && eqlQueryResult != null && showHistogram && !eqlQueryLoading && ( + {ruleType === 'eql' && showHistogram && ( )} - {warnings.length > 0 && - warnings.map((warning) => ( - <> + {showHistogram && + warnings.length > 0 && + warnings.map((warning, i) => ( + - +

{warning}

- +
))} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx deleted file mode 100644 index 6f5f53a37af9b..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiSpacer, EuiText, EuiFlexItem } from '@elastic/eui'; - -import * as i18n from './translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { MatrixHistogram } from '../../../../common/components/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution'; -import { - MatrixHistogramOption, - MatrixHistogramConfigs, -} from '../../../../common/components/matrix_histogram/types'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; - -const ID = 'nonEqlRuleQueryPreviewHistogramQuery'; - -const stackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, -]; -const DEFAULT_STACK_BY = 'event.category'; - -const histogramConfigs: MatrixHistogramConfigs = { - defaultStackByOption: - stackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? stackByOptions[0], - errorMessage: i18n.PREVIEW_QUERY_ERROR, - histogramType: MatrixHistogramType.events, - stackByOptions, - title: i18n.QUERY_GRAPH_HITS_TITLE, - titleSize: 'xs', - subtitle: i18n.QUERY_PREVIEW_TITLE, - hideHistogramIfEmpty: false, -}; - -interface PreviewNonEqlQueryHistogramProps { - to: string; - from: string; - index: string[]; - filterQuery: ESQueryStringQuery | undefined; -} - -export const PreviewNonEqlQueryHistogram = ({ - index, - from, - to, - filterQuery, -}: PreviewNonEqlQueryHistogramProps) => { - const { setQuery } = useGlobalTime(); - - return ( - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER}

-
- - - } - {...histogramConfigs} - /> - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts new file mode 100644 index 0000000000000..f417c172af188 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts @@ -0,0 +1,458 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; + +import * as i18n from './translations'; +import { Action, State, queryPreviewReducer } from './reducer'; +import { initialState } from './'; + +describe('queryPreviewReducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeEach(() => { + moment.tz.setDefault('UTC'); + reducer = queryPreviewReducer(); + }); + + afterEach(() => { + moment.tz.setDefault('Browser'); + }); + + describe('#setQueryInfo', () => { + test('should not update state if queryBar undefined', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: undefined, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update).toEqual(initialState); + }); + + test('should reset showHistogram and warnings if queryBar undefined', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['uh oh'] }, + { + type: 'setQueryInfo', + queryBar: undefined, + index: ['foo-*'], + ruleType: 'query', + } + ); + + expect(update.warnings).toEqual([]); + expect(update.showHistogram).toBeFalsy(); + }); + + test('should reset showHistogram and warnings if queryBar defined', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['uh oh'] }, + { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + } + ); + + expect(update.warnings).toEqual([]); + expect(update.showHistogram).toBeFalsy(); + }); + + test('should pull the query, language, and filters from the action', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.language).toEqual('kuery'); + expect(update.queryString).toEqual('host.name:*'); + expect(update.filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + }); + + test('should create the queryFilter if query type is not eql', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('should set query to empty string if it is not of type string', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: { not: 'a string' }, language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.queryString).toEqual(''); + }); + }); + + describe('#setTimeframeSelect', () => { + test('should update timeframe with that specified in action" ', () => { + const update = reducer(initialState, { + type: 'setTimeframeSelect', + timeframe: 'd', + }); + + expect(update.timeframe).toEqual('d'); + }); + + test('should reset warnings and showHistogram to false" ', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['blah'] }, + { + type: 'setTimeframeSelect', + timeframe: 'd', + } + ); + + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + }); + + describe('#setResetRuleTypeChange', () => { + test('should reset timeframe, warnings, and hide histogram on rule type change" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'eql', + } + ); + + expect(update.showHistogram).toBeFalsy(); + expect(update.timeframe).toEqual('h'); + expect(update.warnings).toEqual([]); + expect(update.showNonEqlHistogram).toBeFalsy(); + }); + + test('should set timeframe options to hour and day if rule type is eql" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'eql', + } + ); + + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is query" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'query', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is saved_query" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'saved_query', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is threshold and no threshold field is specified" ', () => { + const update = reducer( + { + ...initialState, + timeframe: 'd', + showHistogram: true, + warnings: ['blah'], + thresholdFieldExists: false, + }, + { + type: 'setResetRuleTypeChange', + ruleType: 'threshold', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to false and timeframe options to hour, day, and month if rule type is threshold and threshold field is specified" ', () => { + const update = reducer( + { + ...initialState, + timeframe: 'd', + showHistogram: true, + warnings: ['blah'], + thresholdFieldExists: true, + }, + { + type: 'setResetRuleTypeChange', + ruleType: 'threshold', + } + ); + + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + }); + + describe('#setWarnings', () => { + test('should set warnings to that passed in action" ', () => { + const update = reducer(initialState, { + type: 'setWarnings', + warnings: ['bad'], + }); + + expect(update.warnings).toEqual(['bad']); + }); + }); + + describe('#setShowHistogram', () => { + test('should set "setShowHistogram" to false if "action.show" is false', () => { + const update = reducer(initialState, { + type: 'setShowHistogram', + show: false, + }); + + expect(update.showHistogram).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.show" is true', () => { + const update = reducer(initialState, { + type: 'setShowHistogram', + show: true, + }); + + expect(update.showHistogram).toBeTruthy(); + }); + }); + + describe('#setThresholdQueryVals', () => { + test('should set thresholdFieldExists to true if threshold field is defined and not empty string', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeTruthy(); + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set thresholdFieldExists to false if threshold field is not defined', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: undefined, value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeFalsy(); + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set thresholdFieldExists to false if threshold field is empty string', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: ' ', value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeFalsy(); + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to false if ruleType is eql', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'eql', + }); + + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to true if ruleType is query', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'query', + }); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to true if ruleType is saved_query', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'saved_query', + }); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + }); + + describe('#setToFrom', () => { + test('should update to and from times to be an hour apart if timeframe is "h"', () => { + const update = reducer( + { ...initialState, timeframe: 'h' }, + { + type: 'setToFrom', + } + ); + + const dateFrom = moment(update.fromTime); + const dateTo = moment(update.toTime); + const diff = dateFrom.diff(dateTo); + + // 3600000ms = 60 minutes + // Sometimes test returns 3599999 + expect(Math.ceil(diff / 100000) * 100000).toEqual(3600000); + }); + + test('should update to and from times to be a day apart if timeframe is "d"', () => { + const update = reducer( + { ...initialState, timeframe: 'd' }, + { + type: 'setToFrom', + } + ); + + const dateFrom = moment(update.fromTime); + const dateTo = moment(update.toTime); + const diff = dateFrom.diff(dateTo); + + // 86400000 = 24 hours + // Sometimes test returns 86399999 + expect(Math.ceil(diff / 100000) * 100000).toEqual(86400000); + }); + }); + + describe('#setNoiseWarning', () => { + test('should add noise warning', () => { + const update = reducer( + { ...initialState, warnings: ['uh oh'] }, + { + type: 'setNoiseWarning', + } + ); + + expect(update.warnings).toEqual(['uh oh', i18n.QUERY_PREVIEW_NOISE_WARNING]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts new file mode 100644 index 0000000000000..76047a0af5c54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Unit } from '@elastic/datemath'; +import { EuiSelectOption } from '@elastic/eui'; + +import * as i18n from './translations'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; +import { ESQuery } from '../../../../../common/typed_json'; +import { Language, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { FieldValueQueryBar } from '../query_bar'; +import { formatDate } from '../../../../common/components/super_date_picker'; +import { getInfoFromQueryBar, getTimeframeOptions } from './helpers'; +import { Threshold } from '.'; + +export interface State { + timeframeOptions: EuiSelectOption[]; + showHistogram: boolean; + timeframe: Unit; + warnings: string[]; + queryFilter: ESQuery | undefined; + toTime: string; + fromTime: string; + queryString: string; + language: Language; + filters: Filter[]; + thresholdFieldExists: boolean; + showNonEqlHistogram: boolean; +} + +export type Action = + | { + type: 'setQueryInfo'; + queryBar: FieldValueQueryBar | undefined; + index: string[]; + ruleType: Type; + } + | { + type: 'setTimeframeSelect'; + timeframe: Unit; + } + | { + type: 'setResetRuleTypeChange'; + ruleType: Type; + } + | { + type: 'setWarnings'; + warnings: string[]; + } + | { + type: 'setShowHistogram'; + show: boolean; + } + | { + type: 'setThresholdQueryVals'; + threshold: Threshold; + ruleType: Type; + } + | { + type: 'setNoiseWarning'; + } + | { + type: 'setToFrom'; + }; + +export const queryPreviewReducer = () => (state: State, action: Action): State => { + switch (action.type) { + case 'setQueryInfo': { + if (action.queryBar != null) { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + action.queryBar, + action.index, + action.ruleType + ); + + return { + ...state, + queryString, + language, + filters, + queryFilter, + showHistogram: false, + warnings: [], + }; + } + + return { + ...state, + warnings: [], + showHistogram: false, + }; + } + case 'setTimeframeSelect': { + return { + ...state, + timeframe: action.timeframe, + showHistogram: false, + warnings: [], + }; + } + case 'setResetRuleTypeChange': { + const showNonEqlHist = + action.ruleType === 'query' || + action.ruleType === 'saved_query' || + (action.ruleType === 'threshold' && !state.thresholdFieldExists); + + return { + ...state, + showHistogram: false, + timeframe: 'h', + timeframeOptions: getTimeframeOptions(action.ruleType), + showNonEqlHistogram: showNonEqlHist, + warnings: [], + }; + } + case 'setWarnings': { + return { + ...state, + warnings: action.warnings, + }; + } + case 'setShowHistogram': { + return { + ...state, + showHistogram: action.show, + }; + } + case 'setThresholdQueryVals': { + const thresholdField = + action.threshold != null && + action.threshold.field != null && + action.threshold.field.trim() !== ''; + const showNonEqlHist = + action.ruleType === 'query' || + action.ruleType === 'saved_query' || + (action.ruleType === 'threshold' && !thresholdField); + + return { + ...state, + thresholdFieldExists: thresholdField, + showNonEqlHistogram: showNonEqlHist, + showHistogram: false, + warnings: [], + }; + } + case 'setToFrom': { + return { + ...state, + fromTime: formatDate('now'), + toTime: formatDate(`now-1${state.timeframe}`), + }; + } + case 'setNoiseWarning': { + return { + ...state, + warnings: [...state.warnings, i18n.QUERY_PREVIEW_NOISE_WARNING], + }; + } + default: + return state; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx new file mode 100644 index 0000000000000..8a0cfef1b6256 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewThresholdQueryHistogram } from './threshold_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewThresholdQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + }); + + test('it configures buckets data', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').at(0).props().data + ).toEqual([ + { + key: 'hits', + value: [ + { g: 'siem_kibana', x: 'siem_kibana', y: 400 }, + { g: 'bastion00.siem.estc.dev', x: 'bastion00.siem.estc.dev', y: 80225 }, + { g: 'es02.siem.estc.dev', x: 'es02.siem.estc.dev', y: 1228 }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryPreviewThresholdHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx index 03e0656fe06cb..1021c5b8ddcb7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx @@ -5,98 +5,77 @@ */ import React, { useEffect, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { BarChart } from '../../../../common/components/charts/barchart'; import { getThresholdHistogramConfig } from './helpers'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; -import { ChartSeriesConfigs } from '../../../../common/components/charts/common'; +import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common'; +import { InspectResponse } from '../../../../../public/types'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; export const ID = 'queryPreviewThresholdHistogramQuery'; interface PreviewThresholdQueryHistogramProps { - to: string; - from: string; - filterQuery: ESQueryStringQuery | undefined; - threshold: { field: string | undefined; value: number } | undefined; - index: string[]; + isLoading: boolean; + buckets: Array<{ + key: string; + doc_count: number; + }>; + inspect: InspectResponse; + refetch: inputsModel.Refetch; } export const PreviewThresholdQueryHistogram = ({ - from, - to, - filterQuery, - threshold, - index, + buckets, + inspect, + refetch, + isLoading, }: PreviewThresholdQueryHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); - const [isLoading, { inspect, refetch, buckets }] = useMatrixHistogram({ - errorMessage: i18n.PREVIEW_QUERY_ERROR, - endDate: from, - startDate: to, - filterQuery, - indexNames: index, - histogramType: MatrixHistogramType.events, - stackByField: 'event.category', - threshold, - }); - useEffect((): void => { if (!isLoading && !isInitializing) { setQuery({ id: ID, inspect, loading: isLoading, refetch }); } }, [setQuery, inspect, isLoading, isInitializing, refetch]); - const { data, totalCount } = useMemo(() => { - return { - data: buckets.map<{ x: string; y: number; g: string }>(({ key, doc_count: docCount }) => ({ + const { data, totalCount } = useMemo((): { data: ChartSeriesData[]; totalCount: number } => { + const total = buckets.length; + + const dataBuckets = buckets.map<{ x: string; y: number; g: string }>( + ({ key, doc_count: docCount }) => ({ x: key, y: docCount, g: key, - })), - totalCount: buckets.length, + }) + ); + return { + data: [{ key: 'hits', value: dataBuckets }], + totalCount: total, }; }, [buckets]); const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(200), []); + const subtitle = useMemo( + (): string => + isLoading + ? i18n.PREVIEW_SUBTITLE_LOADING + : i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount), + [isLoading, totalCount] + ); + return ( - <> - - - - - - - - - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER}

-
- -
-
-
- + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts index 3e4f389a1883e..7ae75c51dcf5a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts @@ -124,3 +124,10 @@ export const PREVIEW_WARNING_TIMESTAMP = i18n.translate( defaultMessage: 'Unable to find "@timestamp" field on events.', } ); + +export const PREVIEW_SUBTITLE_LOADING = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading', + { + defaultMessage: '...loading', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 8c76e6a2be57f..63b283308dd59 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -131,7 +131,7 @@ const StepDefineRuleComponent: FC = ({ options: { stripEmptyFields: false }, schema, }); - const { getErrors, getFields, getFormData, reset, submit } = form; + const { getFields, getFormData, reset, submit } = form; const [ { index: formIndex, @@ -152,14 +152,13 @@ const StepDefineRuleComponent: FC = ({ } > ]; + const [isQueryBarValid, setIsQueryBarValid] = useState(false); const index = formIndex || initialState.index; const threatIndex = formThreatIndex || initialState.threatIndex; const ruleType = formRuleType || initialState.ruleType; const queryBarQuery = formQuery != null ? formQuery.query.query : '' || initialState.queryBar.query.query; - const errorExists = getErrors().length > 0; const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index); - const [ threatIndexPatternsLoading, { browserFields: threatBrowserFields, indexPatterns: threatIndexPatterns }, @@ -284,6 +283,7 @@ const StepDefineRuleComponent: FC = ({ path="queryBar" component={EqlQueryBar} componentProps={{ + onValidityChange: setIsQueryBarValid, idAria: 'detectionEngineStepDefineRuleEqlQueryBar', isDisabled: isLoading, isLoading: indexPatternsLoading, @@ -319,6 +319,7 @@ const StepDefineRuleComponent: FC = ({ isLoading: indexPatternsLoading, dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', openTimelineSearch, + onValidityChange: setIsQueryBarValid, onCloseTimelineSearch: handleCloseTimelineSearch, }} /> @@ -394,12 +395,12 @@ const StepDefineRuleComponent: FC = ({ <> 0 trusted applications @@ -36,6 +37,7 @@ exports[`control_panel ControlPanel should render list selection correctly 1`] = > 0 trusted applications @@ -62,6 +64,7 @@ exports[`control_panel ControlPanel should render plural count correctly 1`] = ` > 100 trusted applications @@ -88,6 +91,7 @@ exports[`control_panel ControlPanel should render singular count correctly 1`] = > 1 trusted application diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx index 1dd70d766cd85..66928b99c78be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx @@ -22,7 +22,7 @@ export const ControlPanel = memo( return ( - + {i18n.translate('xpack.securitySolution.trustedapps.list.totalCount', { defaultMessage: '{totalItemCount, plural, one {# trusted application} other {# trusted applications}}', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 8b922605e0ab4..b8692df0240fa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -347,7 +347,7 @@ export const CreateTrustedAppForm = memo( + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> { - if (text.length > maxSize) { - return `${text.substr(0, maxSize)}...`; - } else { - return text; - } -}; - const getEntriesColumnDefinitions = (): Array> => [ { field: 'field', @@ -49,7 +42,7 @@ const getEntriesColumnDefinitions = (): Array truncateText: true, textOnly: true, width: '30%', - render(field: MacosLinuxConditionEntry['field'], entry: Entry) { + render(field: Entry['field'], entry: Entry) { return CONDITION_FIELD_TITLE[field]; }, }, @@ -59,18 +52,25 @@ const getEntriesColumnDefinitions = (): Array sortable: false, truncateText: true, width: '20%', - render() { - return i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { - defaultMessage: 'is', - }); + render(field: Entry['operator'], entry: Entry) { + return OPERATOR_TITLE[field]; }, }, { field: 'value', name: ENTRY_PROPERTY_TITLES.value, sortable: false, - truncateText: true, width: '60%', + 'data-test-subj': 'conditionValue', + render(field: Entry['value'], entry: Entry) { + return ( + + ); + }, }, ]; @@ -86,10 +86,18 @@ export const TrustedAppCard = memo(({ trustedApp, onDelete }: TrustedAppCardProp trimTextOverflow(trustedApp.name || '', 100), [trustedApp.name])} - title={trustedApp.name} + value={ + + } + /> + } /> - } /> - + + } + /> trimTextOverflow(trustedApp.description || '', 100), [ - trustedApp.description, - ])} - title={trustedApp.description} + value={ + + } />
+
No items found
+
@@ -25,7 +31,7 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = ` exports[`TrustedAppsGrid renders correctly when failed loading data for the first time 1`] = `
+
Intenal Server Error +
@@ -48,7 +60,7 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the firs exports[`TrustedAppsGrid renders correctly when failed loading data for the second time 1`] = `
+
Intenal Server Error +
@@ -88,11 +106,14 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
+
@@ -126,9 +147,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -386,9 +411,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -646,9 +675,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -906,9 +939,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -1166,9 +1203,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -1426,9 +1467,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -1686,9 +1731,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -1946,9 +1995,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -2206,9 +2259,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -2466,9 +2523,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -2701,6 +2762,9 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
+
+
No items found
+
@@ -2959,7 +3029,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
@@ -3004,9 +3077,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -3264,9 +3341,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -3524,9 +3605,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -3784,9 +3869,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -4044,9 +4133,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -4304,9 +4397,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -4564,9 +4661,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -4824,9 +4925,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -5084,9 +5189,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -5344,9 +5453,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -5579,6 +5692,9 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
+
@@ -5846,9 +5965,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -6106,9 +6229,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -6366,9 +6493,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -6626,9 +6757,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -6886,9 +7021,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -7146,9 +7285,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -7406,9 +7549,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -7666,9 +7813,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -7926,9 +8077,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -8186,9 +8341,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -8421,6 +8580,9 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
+
) => 'MMM D, YYYY @ HH:mm:ss.SSS' } }}> ({ eui: euiLightVars, darkMode: false })}> + + + + diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx index 4664727dd848c..d6827ba24c238 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { FC, memo, useCallback, useEffect } from 'react'; import { EuiTablePagination, EuiFlexGroup, @@ -12,6 +12,7 @@ import { EuiProgress, EuiIcon, EuiText, + EuiSpacer, } from '@elastic/eui'; import { Pagination } from '../../../state'; @@ -64,6 +65,14 @@ const PaginationBar = ({ pagination, onChange }: PaginationBarProps) => { ); }; +const GridMessage: FC = ({ children }) => ( +
+ + {children} + +
+); + export const TrustedAppsGrid = memo(() => { const pagination = useTrustedAppsSelector(getListPagination); const listItems = useTrustedAppsSelector(getListItems); @@ -80,7 +89,7 @@ export const TrustedAppsGrid = memo(() => { })); return ( - + {isLoading && ( @@ -88,27 +97,33 @@ export const TrustedAppsGrid = memo(() => { )} {error && ( -
+ {error} -
+ + )} + {!error && listItems.length === 0 && ( + + {NO_RESULTS_MESSAGE} + )} - {!error && ( - - {listItems.map((item) => ( - - - - ))} - {listItems.length === 0 && ( - - {NO_RESULTS_MESSAGE} - - )} - + {!error && listItems.length > 0 && ( + <> + + + + {listItems.map((item) => ( + + + + ))} + + )}
{!error && pagination.totalItemCount > 0 && ( + + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 794fba9cd7dd7..181b59c65a3d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -647,12 +647,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 0 + + trusted app 0 +
@@ -667,7 +669,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -792,9 +802,11 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -1033,12 +1047,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -1053,7 +1069,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1152,12 +1176,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -1172,7 +1198,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1271,12 +1305,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -1291,7 +1327,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1390,12 +1434,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -1410,7 +1456,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1509,12 +1563,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -1529,7 +1585,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1628,12 +1692,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -1648,7 +1714,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1747,12 +1821,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -1767,7 +1843,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1866,12 +1950,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -1886,7 +1972,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1985,12 +2079,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -2005,7 +2101,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2104,12 +2208,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -2124,7 +2230,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2223,12 +2337,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -2243,7 +2359,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2342,12 +2466,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -2362,7 +2488,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2461,12 +2595,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -2481,7 +2617,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2580,12 +2724,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -2600,7 +2746,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2699,12 +2853,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -2719,7 +2875,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2818,12 +2982,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -2838,7 +3004,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2937,12 +3111,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -2957,7 +3133,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -3056,12 +3240,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -3076,7 +3262,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3175,12 +3369,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -3195,7 +3391,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3654,12 +3858,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 0 + + trusted app 0 +
@@ -3674,7 +3880,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3773,12 +3987,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -3793,7 +4009,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3892,12 +4116,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -3912,7 +4138,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4011,12 +4245,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -4031,7 +4267,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4130,12 +4374,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -4150,7 +4396,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4249,12 +4503,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -4269,7 +4525,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4368,12 +4632,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -4388,7 +4654,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4487,12 +4761,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -4507,7 +4783,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4606,12 +4890,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -4626,7 +4912,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4725,12 +5019,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -4745,7 +5041,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4844,12 +5148,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -4864,7 +5170,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4963,12 +5277,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -4983,7 +5299,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5082,12 +5406,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -5102,7 +5428,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5201,12 +5535,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -5221,7 +5557,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5320,12 +5664,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -5340,7 +5686,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5439,12 +5793,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -5459,7 +5815,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5558,12 +5922,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -5578,7 +5944,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5677,12 +6051,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -5697,7 +6073,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5796,12 +6180,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -5816,7 +6202,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5915,12 +6309,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -5935,7 +6331,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6552,12 +6956,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 0 + + trusted app 0 +
@@ -6572,7 +6978,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -6671,12 +7085,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 1 + + trusted app 1 +
@@ -6691,7 +7107,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6790,12 +7214,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 2 + + trusted app 2 +
@@ -6810,7 +7236,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -6909,12 +7343,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 3 + + trusted app 3 +
@@ -6929,7 +7365,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7028,12 +7472,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 4 + + trusted app 4 +
@@ -7048,7 +7494,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7147,12 +7601,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 5 + + trusted app 5 +
@@ -7167,7 +7623,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7266,12 +7730,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 6 + + trusted app 6 +
@@ -7286,7 +7752,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7385,12 +7859,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 7 + + trusted app 7 +
@@ -7405,7 +7881,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7504,12 +7988,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 8 + + trusted app 8 +
@@ -7524,7 +8010,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7623,12 +8117,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 9 + + trusted app 9 +
@@ -7643,7 +8139,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7742,12 +8246,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 10 + + trusted app 10 +
@@ -7762,7 +8268,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7861,12 +8375,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 11 + + trusted app 11 +
@@ -7881,7 +8397,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7980,12 +8504,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 12 + + trusted app 12 +
@@ -8000,7 +8526,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8099,12 +8633,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 13 + + trusted app 13 +
@@ -8119,7 +8655,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8218,12 +8762,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 14 + + trusted app 14 +
@@ -8238,7 +8784,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8337,12 +8891,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 15 + + trusted app 15 +
@@ -8357,7 +8913,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8456,12 +9020,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 16 + + trusted app 16 +
@@ -8476,7 +9042,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8575,12 +9149,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 17 + + trusted app 17 +
@@ -8595,7 +9171,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8694,12 +9278,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 18 + + trusted app 18 +
@@ -8714,7 +9300,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8813,12 +9407,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 19 + + trusted app 19 +
@@ -8833,7 +9429,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9292,12 +9896,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 0 + + trusted app 0 +
@@ -9312,7 +9918,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9411,12 +10025,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 1 + + trusted app 1 +
@@ -9431,7 +10047,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9530,12 +10154,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 2 + + trusted app 2 +
@@ -9550,7 +10176,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -9649,12 +10283,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 3 + + trusted app 3 +
@@ -9669,7 +10305,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9768,12 +10412,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 4 + + trusted app 4 +
@@ -9788,7 +10434,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9887,12 +10541,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 5 + + trusted app 5 +
@@ -9907,7 +10563,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10006,12 +10670,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 6 + + trusted app 6 +
@@ -10026,7 +10692,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10125,12 +10799,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 7 + + trusted app 7 +
@@ -10145,7 +10821,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10244,12 +10928,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 8 + + trusted app 8 +
@@ -10264,7 +10950,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10363,12 +11057,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 9 + + trusted app 9 +
@@ -10383,7 +11079,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10482,12 +11186,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 10 + + trusted app 10 +
@@ -10502,7 +11208,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10601,12 +11315,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 11 + + trusted app 11 +
@@ -10621,7 +11337,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10720,12 +11444,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 12 + + trusted app 12 +
@@ -10740,7 +11466,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10839,12 +11573,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 13 + + trusted app 13 +
@@ -10859,7 +11595,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10958,12 +11702,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 14 + + trusted app 14 +
@@ -10978,7 +11724,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11077,12 +11831,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 15 + + trusted app 15 +
@@ -11097,7 +11853,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11196,12 +11960,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 16 + + trusted app 16 +
@@ -11216,7 +11982,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -11315,12 +12089,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 17 + + trusted app 17 +
@@ -11335,7 +12111,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11434,12 +12218,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 18 + + trusted app 18 +
@@ -11454,7 +12240,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11553,12 +12347,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 19 + + trusted app 19 +
@@ -11573,7 +12369,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx index d5c829bccb903..977db9e1fff2d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx @@ -27,6 +27,7 @@ import { } from '../../../store/selectors'; import { FormattedDate } from '../../../../../../common/components/formatted_date'; +import { TextFieldValue } from '../../../../../../common/components/text_field_value'; import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from '../../hooks'; @@ -96,13 +97,27 @@ const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => { { field: 'name', name: PROPERTY_TITLES.name, - truncateText: true, + render(value: TrustedApp['name'], record: Immutable) { + return ( + + ); + }, }, { field: 'os', name: PROPERTY_TITLES.os, render(value: TrustedApp['os'], record: Immutable) { - return OS_TITLES[value]; + return ( + + ); }, }, { @@ -121,6 +136,15 @@ const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => { { field: 'created_by', name: PROPERTY_TITLES.created_by, + render(value: TrustedApp['created_by'], record: Immutable) { + return ( + + ); + }, }, { name: ACTIONS_COLUMN_TITLE, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b442704169d06..b2f62c2f1da4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -28,7 +28,9 @@ export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { }), }; -export const CONDITION_FIELD_TITLE: { [K in MacosLinuxConditionEntry['field']]: string } = { +type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; + +export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { 'process.hash.*': i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash', { defaultMessage: 'Hash' } @@ -37,6 +39,16 @@ export const CONDITION_FIELD_TITLE: { [K in MacosLinuxConditionEntry['field']]: 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), + 'process.code_signature': i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', + { defaultMessage: 'Signature' } + ), +}; + +export const OPERATOR_TITLE: { [K in Entry['operator']]: string } = { + included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { + defaultMessage: 'is', + }), }; export const PROPERTY_TITLES: Readonly< diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 96ab695e8d33c..29aa0b111b78a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -5,10 +5,18 @@ */ import { i18n } from '@kbn/i18n'; -import { Store, Action } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; +import { + PluginSetup, + PluginStart, + SetupPlugins, + StartPlugins, + StartServices, + AppObservableLibs, + SubPlugins, +} from './types'; import { AppMountParameters, CoreSetup, @@ -21,14 +29,7 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { - PluginSetup, - PluginStart, - SetupPlugins, - StartPlugins, - StartServices, - AppObservableLibs, -} from './types'; + import { APP_ID, APP_ICON_SOLUTION, @@ -42,9 +43,9 @@ import { APP_PATH, DEFAULT_INDEX_KEY, } from '../common/constants'; + import { ConfigureEndpointPackagePolicy } from './management/pages/policy/view/ingest_manager_integration/configure_package_policy'; -import { State, createStore, createInitialState } from './common/store'; import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; import { @@ -60,20 +61,30 @@ import { IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, } from '../common/search_strategy/index_fields'; +import { SecurityAppStore } from './common/store/store'; export class Plugin implements IPlugin { private kibanaVersion: string; - private store!: Store; constructor(initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { - const APP_NAME = i18n.translate('xpack.securitySolution.security.title', { - defaultMessage: 'Security', - }); + private storage = new Storage(localStorage); + + /** + * Lazily instantiated subPlugins. + * See `subPlugins` method. + */ + private _subPlugins?: SubPlugins; + + /** + * Lazily instantiated `SecurityAppStore`. + * See `store` method. + */ + private _store?: SecurityAppStore; + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { initTelemetry(plugins.usageCollection, APP_ID); if (plugins.home) { @@ -104,21 +115,22 @@ export class Plugin implements IPlugin { - const storage = new Storage(localStorage); + /** + * `StartServices` which are needed by the `renderApp` function when mounting any of the subPlugin applications. + * This is a promise because these aren't available until the `start` lifecycle phase but they are referenced + * in the `setup` lifecycle phase. + */ + const startServices: Promise = (async () => { const [coreStart, startPlugins] = await core.getStartServices(); - if (this.store == null) { - await this.buildStore(coreStart, startPlugins, storage); - } - const services = { + const services: StartServices = { ...coreStart, ...startPlugins, - storage, + storage: this.storage, security: plugins.security, - } as StartServices; - return { coreStart, startPlugins, services, store: this.store, storage }; - }; + }; + return services; + })(); core.application.register({ exactRoute: true, @@ -141,22 +153,16 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { overviewSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { overview: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: overviewSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -169,21 +175,16 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { detectionsSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { detections: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); + return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: detectionsSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -196,21 +197,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { hostsSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { hosts: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: hostsSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -223,21 +218,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { networkSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { network: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: networkSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -250,21 +239,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { timelinesSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { timelines: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: timelinesSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -277,21 +260,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { casesSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { cases: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: casesSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -304,20 +281,14 @@ export class Plugin implements IPlugin { - const [ - { coreStart, startPlugins, store, services }, - { renderApp, composeLibs }, - { managementSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { management: managementSubPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, + services: await startServices, + store: await this.store(coreStart, startPlugins), SubPluginRoutes: managementSubPlugin.start(coreStart, startPlugins).SubPluginRoutes, }); }, @@ -337,7 +308,13 @@ export class Plugin implements IPlugin { - const { resolverPluginSetup } = await import('./resolver'); + /** + * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. + * See https://webpack.js.org/api/module-methods/#magic-comments + */ + const { resolverPluginSetup } = await import( + /* webpackChunkName: "resolver" */ './resolver' + ); return resolverPluginSetup(); }, }; @@ -359,112 +336,137 @@ export class Plugin implements IPlugin { + if (!this._subPlugins) { + const { subPluginClasses } = await this.lazySubPlugins(); + this._subPlugins = { + detections: new subPluginClasses.Detections(), + cases: new subPluginClasses.Cases(), + hosts: new subPluginClasses.Hosts(), + network: new subPluginClasses.Network(), + overview: new subPluginClasses.Overview(), + timelines: new subPluginClasses.Timelines(), + management: new subPluginClasses.Management(), + }; + } + return this._subPlugins; } - private async buildStore(coreStart: CoreStart, startPlugins: StartPlugins, storage: Storage) { - const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); - const [ - { composeLibs }, - kibanaIndexPatterns, - { - detectionsSubPlugin, - hostsSubPlugin, - networkSubPlugin, - timelinesSubPlugin, - managementSubPlugin, - }, - configIndexPatterns, - ] = await Promise.all([ - this.downloadAssets(), - startPlugins.data.indexPatterns.getIdsWithTitle(), - this.downloadSubPlugins(), - startPlugins.data.search - .search( - { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, - { - strategy: 'securitySolutionIndexFields', - } - ) - .toPromise(), - ]); - - const { apolloClient } = composeLibs(coreStart); - const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart }; - const libs$ = new BehaviorSubject(appLibs); - - const detectionsStart = detectionsSubPlugin.start(storage); - const hostsStart = hostsSubPlugin.start(storage); - const networkStart = networkSubPlugin.start(storage); - const timelinesStart = timelinesSubPlugin.start(); - const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); - - const timelineInitialState = { - timeline: { - ...timelinesStart.store.initialState.timeline!, - timelineById: { - ...timelinesStart.store.initialState.timeline!.timelineById, - ...detectionsStart.storageTimelines!.timelineById, - ...hostsStart.storageTimelines!.timelineById, - ...networkStart.storageTimelines!.timelineById, + /** + * Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference. + */ + private async store(coreStart: CoreStart, startPlugins: StartPlugins): Promise { + if (!this._store) { + const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); + const [ + { composeLibs, createStore, createInitialState }, + kibanaIndexPatterns, + { + detections: detectionsSubPlugin, + hosts: hostsSubPlugin, + network: networkSubPlugin, + timelines: timelinesSubPlugin, + management: managementSubPlugin, }, - }, - }; + configIndexPatterns, + ] = await Promise.all([ + this.lazyApplicationDependencies(), + startPlugins.data.indexPatterns.getIdsWithTitle(), + this.subPlugins(), + startPlugins.data.search + .search( + { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, + { + strategy: 'securitySolutionIndexFields', + } + ) + .toPromise(), + ]); + + const { apolloClient } = composeLibs(coreStart); + const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart }; + const libs$ = new BehaviorSubject(appLibs); + + const detectionsStart = detectionsSubPlugin.start(this.storage); + const hostsStart = hostsSubPlugin.start(this.storage); + const networkStart = networkSubPlugin.start(this.storage); + const timelinesStart = timelinesSubPlugin.start(); + const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); + + const timelineInitialState = { + timeline: { + ...timelinesStart.store.initialState.timeline!, + timelineById: { + ...timelinesStart.store.initialState.timeline!.timelineById, + ...detectionsStart.storageTimelines!.timelineById, + ...hostsStart.storageTimelines!.timelineById, + ...networkStart.storageTimelines!.timelineById, + }, + }, + }; - this.store = createStore( - createInitialState( + this._store = createStore( + createInitialState( + { + ...hostsStart.store.initialState, + ...networkStart.store.initialState, + ...timelineInitialState, + ...managementSubPluginStart.store.initialState, + }, + { + kibanaIndexPatterns, + configIndexPatterns: configIndexPatterns.indicesExist, + } + ), { - ...hostsStart.store.initialState, - ...networkStart.store.initialState, - ...timelineInitialState, - ...managementSubPluginStart.store.initialState, + ...hostsStart.store.reducer, + ...networkStart.store.reducer, + ...timelinesStart.store.reducer, + ...managementSubPluginStart.store.reducer, }, - { - kibanaIndexPatterns, - configIndexPatterns: configIndexPatterns.indicesExist, - } - ), - { - ...hostsStart.store.reducer, - ...networkStart.store.reducer, - ...timelinesStart.store.reducer, - ...managementSubPluginStart.store.reducer, - }, - libs$.pipe(pluck('apolloClient')), - libs$.pipe(pluck('kibana')), - storage, - [...(managementSubPluginStart.store.middleware ?? [])] - ); + libs$.pipe(pluck('apolloClient')), + libs$.pipe(pluck('kibana')), + this.storage, + [...(managementSubPluginStart.store.middleware ?? [])] + ); + } + return this._store; } } + +const APP_NAME = i18n.translate('xpack.securitySolution.security.title', { + defaultMessage: 'Security', +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 0664608d73c27..9d72af3109564 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import copy from 'copy-to-clipboard'; import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher @@ -14,10 +13,6 @@ import { urlSearch } from '../test_utilities/url_search'; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -jest.mock('copy-to-clipboard', () => { - return jest.fn(); -}); - describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. @@ -121,8 +116,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); @@ -179,8 +174,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); @@ -288,8 +283,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx index c3474a7724dee..f6a585ea566bb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx @@ -6,11 +6,11 @@ /* eslint-disable react/display-name */ -import { EuiToolTip, EuiPopover } from '@elastic/eui'; +import { EuiToolTip, EuiButtonIcon, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import React, { memo, useState } from 'react'; -import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard'; +import React, { memo, useState, useCallback } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useColors } from '../use_colors'; import { StyledPanel } from '../styles'; @@ -43,8 +43,10 @@ export const CopyablePanelField = memo( ({ textToCopy, content }: { textToCopy: string; content: JSX.Element | string }) => { const { linkColor, copyableFieldBackground } = useColors(); const [isOpen, setIsOpen] = useState(false); + const toasts = useKibana().services.notifications?.toasts; const onMouseEnter = () => setIsOpen(true); + const onMouseLeave = () => setIsOpen(false); const ButtonContent = memo(() => ( )); - const onMouseLeave = () => setIsOpen(false); + const onClick = useCallback( + async (event: React.MouseEvent) => { + try { + await navigator.clipboard.writeText(textToCopy); + } catch (error) { + if (toasts) { + toasts.addError(error, { + title: i18n.translate('xpack.securitySolution.resolver.panel.copyFailureTitle', { + defaultMessage: 'Copy Failure', + }), + }); + } + } + }, + [textToCopy, toasts] + ); return (
@@ -74,10 +91,14 @@ export const CopyablePanelField = memo( defaultMessage: 'Copy to Clipboard', })} > - diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index f40f423359f56..d93b46dcb0620 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -382,7 +382,7 @@ const UnstyledProcessEventDot = React.memo( />
= 2 ? 'euiButton' : 'euiButton euiButton--small'} + className={'euiButton euiButton--small'} id={labelHTMLID} onClick={handleClick} onFocus={handleFocus} diff --git a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts index 25be222e2fe4a..8517459b8aba3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts @@ -66,6 +66,18 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { return contentRectForElement(this); }); + /** + * Mock the global writeText method as it is not available in jsDOM and alows us to track what was copied + */ + const MockClipboard: Clipboard = { + writeText: jest.fn(), + readText: jest.fn(), + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + removeEventListener: jest.fn(), + }; + // @ts-ignore navigator doesn't natively exist on global + global.navigator.clipboard = MockClipboard; /** * A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize` */ diff --git a/x-pack/plugins/security_solution/public/sub_plugins.ts b/x-pack/plugins/security_solution/public/sub_plugins.ts deleted file mode 100644 index 5e7c5e8242fde..0000000000000 --- a/x-pack/plugins/security_solution/public/sub_plugins.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Detections } from './detections'; -import { Cases } from './cases'; -import { Hosts } from './hosts'; -import { Network } from './network'; -import { Overview } from './overview'; -import { Timelines } from './timelines'; -import { Management } from './management'; - -const detectionsSubPlugin = new Detections(); -const casesSubPlugin = new Cases(); -const hostsSubPlugin = new Hosts(); -const networkSubPlugin = new Network(); -const overviewSubPlugin = new Overview(); -const timelinesSubPlugin = new Timelines(); -const managementSubPlugin = new Management(); - -export { - detectionsSubPlugin, - casesSubPlugin, - hostsSubPlugin, - networkSubPlugin, - overviewSubPlugin, - timelinesSubPlugin, - managementSubPlugin, -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index fc0bcb134158c..bf19671687e87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -68,11 +68,10 @@ export interface BodyProps { updateNote: UpdateNote; } -export const hasAdditionalActions = (id: string, eventType?: TimelineEventsType): boolean => - id === TimelineId.detectionsPage || - id === TimelineId.detectionsRulesDetailsPage || - ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? - false); +export const hasAdditionalActions = (id: TimelineId): boolean => + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( + id + ); const EXTRA_WIDTH = 4; // px @@ -86,7 +85,6 @@ export const Body = React.memo( data, docValueFields, eventIdToNoteIds, - eventType, getNotesByIds, graphEventId, isEventViewer = false, @@ -118,9 +116,11 @@ export const Body = React.memo( getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditionalActions(timelineId, eventType) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + hasAdditionalActions(timelineId as TimelineId) + ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH + : 0 ), - [isEventViewer, showCheckboxes, timelineId, eventType] + [isEventViewer, showCheckboxes, timelineId] ); const columnWidths = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index ef5689f494cd0..dfd646353c275 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -61,7 +61,6 @@ const StatefulBodyComponent = React.memo( data, docValueFields, eventIdToNoteIds, - eventType, excludedRowRendererIds, id, isEventViewer = false, @@ -197,7 +196,6 @@ const StatefulBodyComponent = React.memo( data={data} docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} - eventType={eventType} getNotesByIds={getNotesByIds} graphEventId={graphEventId} isEventViewer={isEventViewer} @@ -232,7 +230,6 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.eventType === nextProps.eventType && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && @@ -262,7 +259,6 @@ const makeMapStateToProps = () => { const { columns, eventIdToNoteIds, - eventType, excludedRowRendererIds, graphEventId, isSelectAllChecked, @@ -277,7 +273,6 @@ const makeMapStateToProps = () => { return { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, - eventType, excludedRowRendererIds, graphEventId, isSelectAllChecked, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index 3523e8c0d7aaf..0cd7032596f15 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -199,6 +199,7 @@ const AddDataProviderPopoverComponent: React.FC = ( withTitle panelPaddingSize="none" ownFocus={true} + repositionOnScroll > {content} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx deleted file mode 100644 index bfd76f0a98d46..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { InsertTimelinePopoverComponent } from '.'; - -const onTimelineChange = jest.fn(); -const props = { - isDisabled: false, - onTimelineChange, -}; - -describe('Insert timeline popover ', () => { - it('it renders', () => { - const wrapper = mount(); - expect(wrapper.find('[data-test-subj="insert-timeline-popover"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx deleted file mode 100644 index 11ad54321da88..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; -import React, { memo, useCallback, useMemo, useState } from 'react'; - -import { OpenTimelineResult } from '../../open_timeline/types'; -import { SelectableTimeline } from '../selectable_timeline'; -import * as i18n from '../translations'; -import { TimelineType } from '../../../../../common/types/timeline'; - -interface InsertTimelinePopoverProps { - isDisabled: boolean; - hideUntitled?: boolean; - onTimelineChange: ( - timelineTitle: string, - timelineId: string | null, - graphEventId?: string - ) => void; -} - -type Props = InsertTimelinePopoverProps; - -export const InsertTimelinePopoverComponent: React.FC = ({ - isDisabled, - hideUntitled = false, - onTimelineChange, -}) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const handleClosePopover = useCallback(() => { - setIsPopoverOpen(false); - }, []); - - const handleOpenPopover = useCallback(() => { - setIsPopoverOpen(true); - }, []); - - const insertTimelineButton = useMemo( - () => ( - {i18n.INSERT_TIMELINE}

}> - -
- ), - [handleOpenPopover, isDisabled] - ); - - const handleGetSelectableOptions = useCallback( - ({ timelines }) => [ - ...timelines.map( - (t: OpenTimelineResult, index: number) => - ({ - description: t.description, - favorite: t.favorite, - label: t.title, - id: t.savedObjectId, - key: `${t.title}-${index}`, - title: t.title, - checked: undefined, - } as EuiSelectableOption) - ), - ], - [] - ); - - return ( - - - - ); -}; - -export const InsertTimelinePopover = memo(InsertTimelinePopoverComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 16200f4e5ef9a..d7d8d810f6972 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -335,6 +335,7 @@ const PickEventTypeComponents: React.FC = ({ button={button} isOpen={isPopoverOpen} closePopover={closePopover} + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index 641a4105e1af1..a80576c7237f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -16,7 +16,7 @@ import { EuiFilterGroup, EuiFilterButton, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import { isEmpty, debounce } from 'lodash/fp'; import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react'; import styled from 'styled-components'; @@ -120,9 +120,14 @@ const SelectableTimelineComponent: React.FC = ({ const selectableListOuterRef = useRef(null); const selectableListInnerRef = useRef(null); - const onSearchTimeline = useCallback((val) => { - setSearchTimelineValue(val); - }, []); + const debouncedSetSearchTimelineValue = useMemo(() => debounce(500, setSearchTimelineValue), []); + + const onSearchTimeline = useCallback( + (val) => { + debouncedSetSearchTimelineValue(val); + }, + [debouncedSetSearchTimelineValue] + ); const handleOnToggleOnlyFavorites = useCallback(() => { setOnlyFavorites(!onlyFavorites); @@ -238,7 +243,7 @@ const SelectableTimelineComponent: React.FC = ({ isLoading: loading, placeholder: useMemo(() => i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER(timelineType), [timelineType]), onSearch: onSearchTimeline, - incremental: false, + incremental: true, inputRef: (node: HTMLInputElement | null) => { setSearchRef(node); }, diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index b55b33fce1dcb..80cc014285aec 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from '../../../../src/core/public'; +import { AppFrontendLibs } from './common/lib/lib'; +import { CoreStart } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; @@ -20,11 +21,18 @@ import { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; -import { AppFrontendLibs } from './common/lib/lib'; import { ResolverPluginSetup } from './resolver/types'; import { Inspect } from '../common/search_strategy'; import { MlPluginSetup, MlPluginStart } from '../../ml/public'; +import { Detections } from './detections'; +import { Cases } from './cases'; +import { Hosts } from './hosts'; +import { Network } from './network'; +import { Overview } from './overview'; +import { Timelines } from './timelines'; +import { Management } from './management'; + export interface SetupPlugins { home?: HomePublicPluginSetup; security: SecurityPluginSetup; @@ -62,3 +70,13 @@ export interface AppObservableLibs extends AppFrontendLibs { } export type InspectResponse = Inspect & { response: string[] }; + +export interface SubPlugins { + detections: Detections; + cases: Cases; + hosts: Hosts; + network: Network; + overview: Overview; + timelines: Timelines; + management: Management; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index c9159032a7917..b5d657fe55a1f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -8,14 +8,12 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../types'; import { validateTree, - validateRelatedEvents, validateEvents, validateChildren, validateAncestry, validateAlerts, validateEntities, } from '../../../common/endpoint/schema/resolver'; -import { handleRelatedEvents } from './resolver/related_events'; import { handleChildren } from './resolver/children'; import { handleAncestry } from './resolver/ancestry'; import { handleTree } from './resolver/tree'; @@ -26,17 +24,6 @@ import { handleEvents } from './resolver/events'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - // this route will be removed in favor of the one below - router.post( - { - // @deprecated use `/resolver/events` instead - path: '/api/endpoint/resolver/{id}/events', - validate: validateRelatedEvents, - options: { authRequired: true }, - }, - handleRelatedEvents(log, endpointAppContext) - ); - router.post( { path: '/api/endpoint/resolver/events', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts deleted file mode 100644 index 3ddf8fa4090d6..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/** - * @deprecated use the `events.ts` file's query instead - */ -import { EventsQuery } from './related_events'; -import { PaginationBuilder } from '../utils/pagination'; -import { legacyEventIndexPattern } from './legacy_event_index_pattern'; - -describe('Events query', () => { - it('constructs a legacy multi search query', () => { - const query = new EventsQuery(new PaginationBuilder(1), 'index-pattern', 'endpointID'); - // using any here because otherwise ts complains that it doesn't know what bool and filter are - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const msearch: any = query.buildMSearch('1234'); - expect(msearch[0].index).toBe(legacyEventIndexPattern); - expect(msearch[1].query.bool.filter[0]).toStrictEqual({ - terms: { 'endgame.unique_pid': ['1234'] }, - }); - }); - - it('constructs a non-legacy multi search query', () => { - const query = new EventsQuery(new PaginationBuilder(1), 'index-pattern'); - // using any here because otherwise ts complains that it doesn't know what bool and filter are - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const msearch: any = query.buildMSearch(['1234', '5678']); - expect(msearch[0].index).toBe('index-pattern'); - expect(msearch[1].query.bool.filter[0]).toStrictEqual({ - terms: { 'process.entity_id': ['1234', '5678'] }, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts deleted file mode 100644 index f419c1fb6e1d5..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/** - * @deprecated use the `events.ts` file's query instead - */ -import { SearchResponse } from 'elasticsearch'; -import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { SafeResolverEvent } from '../../../../../common/endpoint/types'; -import { ResolverQuery } from './base'; -import { PaginationBuilder } from '../utils/pagination'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; - -/** - * Builds a query for retrieving related events for a node. - */ -export class EventsQuery extends ResolverQuery { - private readonly kqlQuery: JsonObject[] = []; - - constructor( - private readonly pagination: PaginationBuilder, - indexPattern: string | string[], - endpointID?: string, - kql?: string - ) { - super(indexPattern, endpointID); - if (kql) { - this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); - } - } - - protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { - return { - query: { - bool: { - filter: [ - ...this.kqlQuery, - { - terms: { 'endgame.unique_pid': uniquePIDs }, - }, - { - term: { 'agent.id': endpointID }, - }, - { - term: { 'event.kind': 'event' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), - }; - } - - protected query(entityIDs: string[]): JsonObject { - return { - query: { - bool: { - filter: [ - ...this.kqlQuery, - { - terms: { 'process.entity_id': entityIDs }, - }, - { - term: { 'event.kind': 'event' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - ...this.pagination.buildQueryFields('event.id', 'desc'), - }; - } - - formatResponse(response: SearchResponse): SafeResolverEvent[] { - return this.getResults(response); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts deleted file mode 100644 index 8fd9ab9a5ccd3..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * @deprecated use the `resolver/events` route and handler instead - */ -import { TypeOf } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; -import { validateRelatedEvents } from '../../../../common/endpoint/schema/resolver'; -import { Fetcher } from './utils/fetch'; -import { EndpointAppContext } from '../../types'; - -export function handleRelatedEvents( - log: Logger, - endpointAppContext: EndpointAppContext -): RequestHandler< - TypeOf, - TypeOf, - TypeOf -> { - return async (context, req, res) => { - const { - params: { id }, - query: { events, afterEvent, legacyEndpointID: endpointID }, - body, - } = req; - try { - const client = context.core.elasticsearch.legacy.client; - - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); - - return res.ok({ - body: await fetcher.events(events, afterEvent, body?.filter), - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } - }; -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts deleted file mode 100644 index a5aa9b6c288c8..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/** - * @deprecated msearch functionality for querying events will be removed shortly - */ -import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; -import { SafeResolverRelatedEvents, SafeResolverEvent } from '../../../../../common/endpoint/types'; -import { createRelatedEvents } from './node'; -import { EventsQuery } from '../queries/related_events'; -import { PaginationBuilder } from './pagination'; -import { QueryInfo } from '../queries/multi_searcher'; -import { SingleQueryHandler } from './fetch'; - -/** - * Parameters for the RelatedEventsQueryHandler - */ -export interface RelatedEventsParams { - limit: number; - entityID: string; - indexPattern: string; - after?: string; - legacyEndpointID?: string; - filter?: string; -} - -/** - * This retrieves the related events for the origin node of a resolver tree. - */ -export class RelatedEventsQueryHandler implements SingleQueryHandler { - private relatedEvents: SafeResolverRelatedEvents | undefined; - private readonly query: EventsQuery; - private readonly limit: number; - private readonly entityID: string; - - constructor(options: RelatedEventsParams) { - this.limit = options.limit; - this.entityID = options.entityID; - - this.query = new EventsQuery( - PaginationBuilder.createBuilder(this.limit, options.after), - options.indexPattern, - options.legacyEndpointID, - options.filter - ); - } - - private handleResponse = (response: SearchResponse) => { - const results = this.query.formatResponse(response); - this.relatedEvents = createRelatedEvents( - this.entityID, - results, - PaginationBuilder.buildCursorRequestLimit(this.limit, results) - ); - }; - - /** - * Get a query to use in a msearch. - */ - nextQuery(): QueryInfo | undefined { - if (this.getResults()) { - return; - } - - return { - query: this.query, - ids: this.entityID, - handler: this.handleResponse, - }; - } - - /** - * Get the results after an msearch. - */ - getResults() { - return this.relatedEvents; - } - - /** - * Perform a normal search and return the related events results. - * - * @param client the elasticsearch client - */ - async search(client: ILegacyScopedClusterClient) { - const results = this.getResults(); - if (results) { - return results; - } - - this.handleResponse(await this.query.search(client, this.entityID)); - return this.getResults() ?? createRelatedEvents(this.entityID); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 15a9639872f2a..8f17a20e182ad 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -7,7 +7,6 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { SafeResolverChildren, - SafeResolverRelatedEvents, SafeResolverAncestry, ResolverRelatedAlerts, SafeResolverLifecycleNode, @@ -18,7 +17,6 @@ import { StatsQuery } from '../queries/stats'; import { createLifecycle } from './node'; import { MultiSearcher, QueryInfo } from '../queries/multi_searcher'; import { AncestryQueryHandler } from './ancestry_query_handler'; -import { RelatedEventsQueryHandler } from './events_query_handler'; import { RelatedAlertsQueryHandler } from './alerts_query_handler'; import { ChildrenStartQueryHandler } from './children_start_query_handler'; import { ChildrenLifecycleQueryHandler } from './children_lifecycle_query_handler'; @@ -110,14 +108,6 @@ export class Fetcher { this.endpointID ); - const eventsHandler = new RelatedEventsQueryHandler({ - limit: options.events, - entityID: this.id, - after: options.afterEvent, - indexPattern: this.eventsIndexPattern, - legacyEndpointID: this.endpointID, - }); - const alertsHandler = new RelatedAlertsQueryHandler({ limit: options.alerts, entityID: this.id, @@ -139,7 +129,6 @@ export class Fetcher { const msearch = new MultiSearcher(this.client); let queries: QueryInfo[] = []; - addQueryToList(eventsHandler, queries); addQueryToList(alertsHandler, queries); addQueryToList(childrenHandler, queries); addQueryToList(originHandler, queries); @@ -176,7 +165,6 @@ export class Fetcher { const tree = new Tree(this.id, { ancestry: ancestryHandler.getResults(), - relatedEvents: eventsHandler.getResults(), relatedAlerts: alertsHandler.getResults(), children: childrenLifecycleHandler.getResults(), }); @@ -225,31 +213,6 @@ export class Fetcher { return childrenLifecycleHandler.search(this.client); } - /** - * Retrieves the related events for the origin node. - * - * @param limit the upper bound number of related events to return. The limit is applied after the cursor is used to - * skip the previous results. - * @param after a cursor to use as the starting point for retrieving related events - * @param filter a kql query for filtering the results - */ - public async events( - limit: number, - after?: string, - filter?: string - ): Promise { - const eventsHandler = new RelatedEventsQueryHandler({ - limit, - entityID: this.id, - after, - indexPattern: this.eventsIndexPattern, - legacyEndpointID: this.endpointID, - filter, - }); - - return eventsHandler.search(this.client); - } - /** * Retrieves the alerts for the origin node. * diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index cecdc8a478958..286564d9302c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -12,25 +12,9 @@ import { SafeResolverLifecycleNode, SafeResolverEvent, SafeResolverChildNode, - SafeResolverRelatedEvents, ResolverPaginatedEvents, } from '../../../../../common/endpoint/types'; -/** - * Creates a related event object that the related events handler would return - * - * @param entityID the entity_id for these related events - * @param events array of related events - * @param nextEvent the cursor to retrieve the next related event - */ -export function createRelatedEvents( - entityID: string, - events: SafeResolverEvent[] = [], - nextEvent: string | null = null -): SafeResolverRelatedEvents { - return { entityID, events, nextEvent }; -} - /** * Creates an object that the events handler would return * @@ -116,10 +100,6 @@ export function createTree(entityID: string): SafeResolverTree { childNodes: [], nextChild: null, }, - relatedEvents: { - events: [], - nextEvent: null, - }, relatedAlerts: { alerts: [], nextAlert: null, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts index 290af87a61b1d..ce933380e9f34 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts @@ -6,11 +6,7 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { Tree } from './tree'; -import { - SafeResolverAncestry, - SafeResolverEvent, - SafeResolverRelatedEvents, -} from '../../../../../common/endpoint/types'; +import { SafeResolverAncestry, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { entityIDSafeVersion } from '../../../../../common/endpoint/models/event'; describe('Tree', () => { @@ -46,19 +42,4 @@ describe('Tree', () => { expect(tree.render().ancestry.nextAncestor).toEqual('hello'); }); }); - - describe('related events', () => { - it('adds related events to the tree', () => { - const root = generator.generateEvent(); - const events: SafeResolverRelatedEvents = { - entityID: entityIDSafeVersion(root) ?? '', - events: Array.from(generator.relatedEventsGenerator(root)), - nextEvent: null, - }; - const tree = new Tree(entityIDSafeVersion(root) ?? '', { relatedEvents: events }); - const rendered = tree.render(); - expect(rendered.relatedEvents.nextEvent).toBeNull(); - expect(rendered.relatedEvents.events).toStrictEqual(events.events); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index dd493d70ffcd3..26ac15e73759a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -8,7 +8,6 @@ import _ from 'lodash'; import { SafeResolverEvent, ResolverNodeStats, - SafeResolverRelatedEvents, SafeResolverAncestry, SafeResolverTree, SafeResolverChildren, @@ -23,7 +22,6 @@ interface Node { } export interface Options { - relatedEvents?: SafeResolverRelatedEvents; ancestry?: SafeResolverAncestry; children?: SafeResolverChildren; relatedAlerts?: ResolverRelatedAlerts; @@ -44,7 +42,6 @@ export class Tree { this.tree = tree; this.cache.set(id, tree); - this.addRelatedEvents(options.relatedEvents); this.addAncestors(options.ancestry); this.addChildren(options.children); this.addRelatedAlerts(options.relatedAlerts); @@ -68,20 +65,6 @@ export class Tree { return [...this.cache.keys()]; } - /** - * Add related events for the tree's origin node. Related events cannot be added for other nodes. - * - * @param relatedEventsInfo is the related events and pagination information to add to the tree. - */ - private addRelatedEvents(relatedEventsInfo: SafeResolverRelatedEvents | undefined) { - if (!relatedEventsInfo) { - return; - } - - this.tree.relatedEvents.events = relatedEventsInfo.events; - this.tree.relatedEvents.nextEvent = relatedEventsInfo.nextEvent; - } - /** * Add alerts for the tree's origin node. Alerts cannot be added for other nodes. * diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 09ddfb342496d..12f4ae124ed31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { getThreatList } from './get_threat_list'; import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; -import { CreateThreatSignalOptions, ThreatListItem } from './types'; +import { CreateThreatSignalOptions, ThreatSignalResults } from './types'; import { combineResults } from './utils'; -import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ threatMapping, @@ -51,57 +49,7 @@ export const createThreatSignal = async ({ name, currentThreatList, currentResult, -}: CreateThreatSignalOptions): Promise<{ - threatList: SearchResponse; - results: SearchAfterAndBulkCreateReturnType; -}> => { - const threatFilter = buildThreatMappingFilter({ - threatMapping, - threatList: currentThreatList, - }); - - const esFilter = await getFilter({ - type, - filters: [...filters, threatFilter], - language, - query, - savedId, - services, - index: inputIndex, - lists: exceptionItems, - }); - - const newResult = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, - listClient, - exceptionsList: exceptionItems, - ruleParams: params, - services, - logger, - eventsTelemetry, - id: alertId, - inputIndexPattern: inputIndex, - signalsIndex: outputIndex, - filter: esFilter, - actions, - name, - createdBy, - createdAt, - updatedBy, - updatedAt, - interval, - enabled, - pageSize: searchAfterSize, - refresh, - tags, - throttle, - buildRuleMessage, - }); - - const results = combineResults(currentResult, newResult); - const searchAfter = currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort; - +}: CreateThreatSignalOptions): Promise => { const threatList = await getThreatList({ callCluster: services.callCluster, exceptionItems, @@ -109,10 +57,59 @@ export const createThreatSignal = async ({ language: threatLanguage, threatFilters, index: threatIndex, - searchAfter, + searchAfter: currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort, sortField: undefined, sortOrder: undefined, }); - return { threatList, results }; + const threatFilter = buildThreatMappingFilter({ + threatMapping, + threatList: currentThreatList, + }); + + if (threatFilter.query.bool.should.length === 0) { + // empty threat list and we do not want to return everything as being + // a hit so opt to return the existing result. + return { threatList, results: currentResult }; + } else { + const esFilter = await getFilter({ + type, + filters: [...filters, threatFilter], + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems, + }); + const newResult = await searchAfterAndBulkCreate({ + gap, + previousStartedAt, + listClient, + exceptionsList: exceptionItems, + ruleParams: params, + services, + logger, + eventsTelemetry, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + filter: esFilter, + actions, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + pageSize: searchAfterSize, + refresh, + tags, + throttle, + buildRuleMessage, + }); + const results = combineResults(currentResult, newResult); + return { threatList, results }; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 06c9c4c13c5f3..da7aab3ad56e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -103,6 +103,11 @@ export interface CreateThreatSignalOptions { currentResult: SearchAfterAndBulkCreateReturnType; } +export interface ThreatSignalResults { + threatList: SearchResponse; + results: SearchAfterAndBulkCreateReturnType; +} + export interface BuildThreatMappingFilterOptions { threatMapping: ThreatMapping; threatList: SearchResponse; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts index da29cae0eebeb..bc461f3885a70 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import isEmpty from 'lodash/isEmpty'; import { IndexPatternsFetcher, ISearchStrategy } from '../../../../../../src/plugins/data/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -25,60 +26,63 @@ export const securitySolutionIndexFieldsProvider = (): ISearchStrategy< const beatFields: BeatFields = require('../../utils/beat_schema/fields').fieldsBeat; return { - search: async (context, request) => { - const { elasticsearch } = context.core; - const indexPatternsFetcher = new IndexPatternsFetcher( - elasticsearch.legacy.client.callAsCurrentUser - ); - const dedupeIndices = dedupeIndexName(request.indices); + search: (request, options, context) => + from( + new Promise(async (resolve) => { + const { elasticsearch } = context.core; + const indexPatternsFetcher = new IndexPatternsFetcher( + elasticsearch.legacy.client.callAsCurrentUser + ); + const dedupeIndices = dedupeIndexName(request.indices); - const responsesIndexFields = await Promise.all( - dedupeIndices - .map((index) => - indexPatternsFetcher.getFieldsForWildcard({ - pattern: index, - }) - ) - .map((p) => p.catch((e) => false)) - ); - let indexFields: IndexField[] = []; + const responsesIndexFields = await Promise.all( + dedupeIndices + .map((index) => + indexPatternsFetcher.getFieldsForWildcard({ + pattern: index, + }) + ) + .map((p) => p.catch((e) => false)) + ); + let indexFields: IndexField[] = []; - if (!request.onlyCheckIfIndicesExist) { - indexFields = await formatIndexFields( - beatFields, - responsesIndexFields.filter((rif) => rif !== false) as FieldDescriptor[][], - dedupeIndices - ); - } + if (!request.onlyCheckIfIndicesExist) { + indexFields = await formatIndexFields( + beatFields, + responsesIndexFields.filter((rif) => rif !== false) as FieldDescriptor[][], + dedupeIndices + ); + } - return Promise.resolve({ - indexFields, - indicesExist: dedupeIndices.filter((index, i) => responsesIndexFields[i] !== false), - rawResponse: { - timed_out: false, - took: -1, - _shards: { - total: -1, - successful: -1, - failed: -1, - skipped: -1, - }, - hits: { - total: -1, - max_score: -1, - hits: [ - { - _index: '', - _type: '', - _id: '', - _score: -1, - _source: null, + return resolve({ + indexFields, + indicesExist: dedupeIndices.filter((index, i) => responsesIndexFields[i] !== false), + rawResponse: { + timed_out: false, + took: -1, + _shards: { + total: -1, + successful: -1, + failed: -1, + skipped: -1, }, - ], - }, - }, - }); - }, + hits: { + total: -1, + max_score: -1, + hits: [ + { + _index: '', + _type: '', + _id: '', + _score: -1, + _source: null, + }, + ], + }, + }, + }); + }) + ), }; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index d94a32174cd7a..962865880df5f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mergeMap } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; import { FactoryQueryTypes, @@ -19,15 +20,16 @@ export const securitySolutionSearchStrategyProvider = { + search: (request, options, context) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } const queryFactory: SecuritySolutionFactory = securitySolutionFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - const esSearchRes = await es.search(context, { ...request, params: dsl }, options); - return queryFactory.parse(request, esSearchRes); + return es + .search({ ...request, params: dsl }, options, context) + .pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))); }, cancel: async (context, id) => { if (es.cancel) { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index 6d8505211123b..165f0f586ebdb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mergeMap } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; import { TimelineFactoryQueryTypes, @@ -19,15 +20,17 @@ export const securitySolutionTimelineSearchStrategyProvider = { + search: (request, options, context) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } const queryFactory: SecuritySolutionTimelineFactory = securitySolutionTimelineFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - const esSearchRes = await es.search(context, { ...request, params: dsl }, options); - return queryFactory.parse(request, esSearchRes); + + return es + .search({ ...request, params: dsl }, options, context) + .pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))); }, cancel: async (context, id) => { if (es.cancel) { diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9514233bdfa86..a2e34229f7d74 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; +import { CoreSetup } from '../../../../../src/core/server'; +import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server'; import { CollectorDependencies } from './types'; import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; @@ -77,7 +78,7 @@ export const registerCollector: RegisterCollector = ({ }, }, isReady: () => kibanaIndex.length > 0, - fetch: async (callCluster: LegacyAPICaller): Promise => { + fetch: async ({ callCluster }: CollectorFetchContext): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); const [detections, endpoints] = await Promise.allSettled([ fetchDetectionsUsage(kibanaIndex, callCluster, ml), diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index fddd7f92b7f27..864c91c583e82 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -10,6 +10,7 @@ import { PluginsSetup } from '../plugin'; import { KibanaFeature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; interface SetupOpts { license?: Partial; @@ -67,6 +68,13 @@ const defaultCallClusterMock = jest.fn().mockResolvedValue({ }, }); +const getMockFetchContext = (mockedCallCluster: jest.Mock) => { + return { + ...createCollectorFetchContextMock(), + callCluster: mockedCallCluster, + }; +}; + describe('error handling', () => { it('handles a 404 when searching for space usage', async () => { const { features, licensing, usageCollecion } = setup({ @@ -78,7 +86,7 @@ describe('error handling', () => { licensing, }); - await getSpacesUsage(jest.fn().mockRejectedValue({ status: 404 })); + await getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); }); it('throws error for a non-404', async () => { @@ -94,7 +102,9 @@ describe('error handling', () => { const statusCodes = [401, 402, 403, 500]; for (const statusCode of statusCodes) { const error = { status: statusCode }; - await expect(getSpacesUsage(jest.fn().mockRejectedValue(error))).rejects.toBe(error); + await expect( + getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue(error))) + ).rejects.toBe(error); } }); }); @@ -110,7 +120,7 @@ describe('with a basic license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { body: { @@ -158,7 +168,7 @@ describe('with no license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to false', () => { @@ -189,7 +199,7 @@ describe('with platinum license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to true', () => { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 36d46c3d01baf..0e31c930a926b 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -6,7 +6,7 @@ import { LegacyCallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { PluginsSetup } from '../plugin'; @@ -188,7 +188,7 @@ export function getSpacesUsageCollector( enabled: { type: 'boolean' }, count: { type: 'long' }, }, - fetch: async (callCluster: CallCluster) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const license = await deps.licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts new file mode 100644 index 0000000000000..443c811469002 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { mockLogger } from '../test_utils'; +import { TaskManager } from '../task_manager'; +import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectsErrorHelpers, +} from '../../../../../src/core/server'; +import { ADJUST_THROUGHPUT_INTERVAL } from '../lib/create_managed_configuration'; + +describe('managed configuration', () => { + let taskManager: TaskManager; + let clock: sinon.SinonFakeTimers; + const callAsInternalUser = jest.fn(); + const logger = mockLogger(); + const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); + const savedObjectsClient = savedObjectsRepositoryMock.create(); + const config = { + enabled: true, + max_workers: 10, + index: 'foo', + max_attempts: 9, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + }; + + beforeEach(() => { + jest.resetAllMocks(); + callAsInternalUser.mockResolvedValue({ total: 0, updated: 0, version_conflicts: 0 }); + clock = sinon.useFakeTimers(); + taskManager = new TaskManager({ + config, + logger, + serializer, + callAsInternalUser, + taskManagerId: 'some-uuid', + savedObjectsRepository: savedObjectsClient, + }); + taskManager.registerTaskDefinitions({ + foo: { + type: 'foo', + title: 'Foo', + createTaskRunner: jest.fn(), + }, + }); + taskManager.start(); + // force rxjs timers to fire when they are scheduled for setTimeout(0) as the + // sinon fake timers cause them to stall + clock.tick(0); + }); + + afterEach(() => clock.restore()); + + test('should lower max workers when Elasticsearch returns 429 error', async () => { + savedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b') + ); + // Cause "too many requests" error to be thrown + await expect( + taskManager.schedule({ + taskType: 'foo', + state: {}, + params: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Max workers configuration is temporarily reduced after Elasticsearch returned 1 "too many request" error(s).' + ); + expect(logger.debug).toHaveBeenCalledWith( + 'Max workers configuration changing from 10 to 8 after seeing 1 error(s)' + ); + expect(logger.debug).toHaveBeenCalledWith('Task pool now using 10 as the max worker value'); + }); + + test('should increase poll interval when Elasticsearch returns 429 error', async () => { + savedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b') + ); + // Cause "too many requests" error to be thrown + await expect( + taskManager.schedule({ + taskType: 'foo', + state: {}, + params: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Poll interval configuration is temporarily increased after Elasticsearch returned 1 "too many request" error(s).' + ); + expect(logger.debug).toHaveBeenCalledWith( + 'Poll interval configuration changing from 3000 to 3600 after seeing 1 error(s)' + ); + expect(logger.debug).toHaveBeenCalledWith('Task poller now using interval of 3600ms'); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts new file mode 100644 index 0000000000000..b6b5cd003c5d4 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { Subject } from 'rxjs'; +import { mockLogger } from '../test_utils'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { + createManagedConfiguration, + ADJUST_THROUGHPUT_INTERVAL, +} from './create_managed_configuration'; + +describe('createManagedConfiguration()', () => { + let clock: sinon.SinonFakeTimers; + const logger = mockLogger(); + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('returns observables with initialized values', async () => { + const maxWorkersSubscription = jest.fn(); + const pollIntervalSubscription = jest.fn(); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + logger, + errors$: new Subject(), + startingMaxWorkers: 1, + startingPollInterval: 2, + }); + maxWorkersConfiguration$.subscribe(maxWorkersSubscription); + pollIntervalConfiguration$.subscribe(pollIntervalSubscription); + expect(maxWorkersSubscription).toHaveBeenCalledTimes(1); + expect(maxWorkersSubscription).toHaveBeenNthCalledWith(1, 1); + expect(pollIntervalSubscription).toHaveBeenCalledTimes(1); + expect(pollIntervalSubscription).toHaveBeenNthCalledWith(1, 2); + }); + + test(`skips errors that aren't about too many requests`, async () => { + const maxWorkersSubscription = jest.fn(); + const pollIntervalSubscription = jest.fn(); + const errors$ = new Subject(); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + errors$, + logger, + startingMaxWorkers: 100, + startingPollInterval: 100, + }); + maxWorkersConfiguration$.subscribe(maxWorkersSubscription); + pollIntervalConfiguration$.subscribe(pollIntervalSubscription); + errors$.next(new Error('foo')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(maxWorkersSubscription).toHaveBeenCalledTimes(1); + expect(pollIntervalSubscription).toHaveBeenCalledTimes(1); + }); + + describe('maxWorker configuration', () => { + function setupScenario(startingMaxWorkers: number) { + const errors$ = new Subject(); + const subscription = jest.fn(); + const { maxWorkersConfiguration$ } = createManagedConfiguration({ + errors$, + startingMaxWorkers, + logger, + startingPollInterval: 1, + }); + maxWorkersConfiguration$.subscribe(subscription); + return { subscription, errors$ }; + } + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('should decrease configuration at the next interval when an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL - 1); + expect(subscription).toHaveBeenCalledTimes(1); + clock.tick(1); + expect(subscription).toHaveBeenCalledTimes(2); + expect(subscription).toHaveBeenNthCalledWith(2, 80); + }); + + test('should log a warning when the configuration changes from the starting value', async () => { + const { errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Max workers configuration is temporarily reduced after Elasticsearch returned 1 "too many request" error(s).' + ); + }); + + test('should increase configuration back to normal incrementally after an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL * 10); + expect(subscription).toHaveBeenNthCalledWith(2, 80); + expect(subscription).toHaveBeenNthCalledWith(3, 84); + // 88.2- > 89 from Math.ceil + expect(subscription).toHaveBeenNthCalledWith(4, 89); + expect(subscription).toHaveBeenNthCalledWith(5, 94); + expect(subscription).toHaveBeenNthCalledWith(6, 99); + // 103.95 -> 100 from Math.min with starting value + expect(subscription).toHaveBeenNthCalledWith(7, 100); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(7); + }); + + test('should keep reducing configuration when errors keep emitting', async () => { + const { subscription, errors$ } = setupScenario(100); + for (let i = 0; i < 20; i++) { + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + } + expect(subscription).toHaveBeenNthCalledWith(2, 80); + expect(subscription).toHaveBeenNthCalledWith(3, 64); + // 51.2 -> 51 from Math.floor + expect(subscription).toHaveBeenNthCalledWith(4, 51); + expect(subscription).toHaveBeenNthCalledWith(5, 40); + expect(subscription).toHaveBeenNthCalledWith(6, 32); + expect(subscription).toHaveBeenNthCalledWith(7, 25); + expect(subscription).toHaveBeenNthCalledWith(8, 20); + expect(subscription).toHaveBeenNthCalledWith(9, 16); + expect(subscription).toHaveBeenNthCalledWith(10, 12); + expect(subscription).toHaveBeenNthCalledWith(11, 9); + expect(subscription).toHaveBeenNthCalledWith(12, 7); + expect(subscription).toHaveBeenNthCalledWith(13, 5); + expect(subscription).toHaveBeenNthCalledWith(14, 4); + expect(subscription).toHaveBeenNthCalledWith(15, 3); + expect(subscription).toHaveBeenNthCalledWith(16, 2); + expect(subscription).toHaveBeenNthCalledWith(17, 1); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(17); + }); + }); + + describe('pollInterval configuration', () => { + function setupScenario(startingPollInterval: number) { + const errors$ = new Subject(); + const subscription = jest.fn(); + const { pollIntervalConfiguration$ } = createManagedConfiguration({ + logger, + errors$, + startingPollInterval, + startingMaxWorkers: 1, + }); + pollIntervalConfiguration$.subscribe(subscription); + return { subscription, errors$ }; + } + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('should increase configuration at the next interval when an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL - 1); + expect(subscription).toHaveBeenCalledTimes(1); + clock.tick(1); + expect(subscription).toHaveBeenCalledTimes(2); + expect(subscription).toHaveBeenNthCalledWith(2, 120); + }); + + test('should log a warning when the configuration changes from the starting value', async () => { + const { errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Poll interval configuration is temporarily increased after Elasticsearch returned 1 "too many request" error(s).' + ); + }); + + test('should decrease configuration back to normal incrementally after an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL * 10); + expect(subscription).toHaveBeenNthCalledWith(2, 120); + expect(subscription).toHaveBeenNthCalledWith(3, 114); + // 108.3 -> 108 from Math.floor + expect(subscription).toHaveBeenNthCalledWith(4, 108); + expect(subscription).toHaveBeenNthCalledWith(5, 102); + // 96.9 -> 100 from Math.max with the starting value + expect(subscription).toHaveBeenNthCalledWith(6, 100); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(6); + }); + + test('should increase configuration when errors keep emitting', async () => { + const { subscription, errors$ } = setupScenario(100); + for (let i = 0; i < 3; i++) { + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + } + expect(subscription).toHaveBeenNthCalledWith(2, 120); + expect(subscription).toHaveBeenNthCalledWith(3, 144); + // 172.8 -> 173 from Math.ceil + expect(subscription).toHaveBeenNthCalledWith(4, 173); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts new file mode 100644 index 0000000000000..3dc5fd50d3ca4 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { interval, merge, of, Observable } from 'rxjs'; +import { filter, mergeScan, map, scan, distinctUntilChanged, startWith } from 'rxjs/operators'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { Logger } from '../types'; + +const FLUSH_MARKER = Symbol('flush'); +export const ADJUST_THROUGHPUT_INTERVAL = 10 * 1000; + +// When errors occur, reduce maxWorkers by MAX_WORKERS_DECREASE_PERCENTAGE +// When errors no longer occur, start increasing maxWorkers by MAX_WORKERS_INCREASE_PERCENTAGE +// until starting value is reached +const MAX_WORKERS_DECREASE_PERCENTAGE = 0.8; +const MAX_WORKERS_INCREASE_PERCENTAGE = 1.05; + +// When errors occur, increase pollInterval by POLL_INTERVAL_INCREASE_PERCENTAGE +// When errors no longer occur, start decreasing pollInterval by POLL_INTERVAL_DECREASE_PERCENTAGE +// until starting value is reached +const POLL_INTERVAL_DECREASE_PERCENTAGE = 0.95; +const POLL_INTERVAL_INCREASE_PERCENTAGE = 1.2; + +interface ManagedConfigurationOpts { + logger: Logger; + startingMaxWorkers: number; + startingPollInterval: number; + errors$: Observable; +} + +interface ManagedConfiguration { + maxWorkersConfiguration$: Observable; + pollIntervalConfiguration$: Observable; +} + +export function createManagedConfiguration({ + logger, + startingMaxWorkers, + startingPollInterval, + errors$, +}: ManagedConfigurationOpts): ManagedConfiguration { + const errorCheck$ = countErrors(errors$, ADJUST_THROUGHPUT_INTERVAL); + return { + maxWorkersConfiguration$: errorCheck$.pipe( + createMaxWorkersScan(logger, startingMaxWorkers), + startWith(startingMaxWorkers), + distinctUntilChanged() + ), + pollIntervalConfiguration$: errorCheck$.pipe( + createPollIntervalScan(logger, startingPollInterval), + startWith(startingPollInterval), + distinctUntilChanged() + ), + }; +} + +function createMaxWorkersScan(logger: Logger, startingMaxWorkers: number) { + return scan((previousMaxWorkers: number, errorCount: number) => { + let newMaxWorkers: number; + if (errorCount > 0) { + // Decrease max workers by MAX_WORKERS_DECREASE_PERCENTAGE while making sure it doesn't go lower than 1. + // Using Math.floor to make sure the number is different than previous while not being a decimal value. + newMaxWorkers = Math.max(Math.floor(previousMaxWorkers * MAX_WORKERS_DECREASE_PERCENTAGE), 1); + } else { + // Increase max workers by MAX_WORKERS_INCREASE_PERCENTAGE while making sure it doesn't go + // higher than the starting value. Using Math.ceil to make sure the number is different than + // previous while not being a decimal value + newMaxWorkers = Math.min( + startingMaxWorkers, + Math.ceil(previousMaxWorkers * MAX_WORKERS_INCREASE_PERCENTAGE) + ); + } + if (newMaxWorkers !== previousMaxWorkers) { + logger.debug( + `Max workers configuration changing from ${previousMaxWorkers} to ${newMaxWorkers} after seeing ${errorCount} error(s)` + ); + if (previousMaxWorkers === startingMaxWorkers) { + logger.warn( + `Max workers configuration is temporarily reduced after Elasticsearch returned ${errorCount} "too many request" error(s).` + ); + } + } + return newMaxWorkers; + }, startingMaxWorkers); +} + +function createPollIntervalScan(logger: Logger, startingPollInterval: number) { + return scan((previousPollInterval: number, errorCount: number) => { + let newPollInterval: number; + if (errorCount > 0) { + // Increase poll interval by POLL_INTERVAL_INCREASE_PERCENTAGE and use Math.ceil to + // make sure the number is different than previous while not being a decimal value. + newPollInterval = Math.ceil(previousPollInterval * POLL_INTERVAL_INCREASE_PERCENTAGE); + } else { + // Decrease poll interval by POLL_INTERVAL_DECREASE_PERCENTAGE and use Math.floor to + // make sure the number is different than previous while not being a decimal value. + newPollInterval = Math.max( + startingPollInterval, + Math.floor(previousPollInterval * POLL_INTERVAL_DECREASE_PERCENTAGE) + ); + } + if (newPollInterval !== previousPollInterval) { + logger.debug( + `Poll interval configuration changing from ${previousPollInterval} to ${newPollInterval} after seeing ${errorCount} error(s)` + ); + if (previousPollInterval === startingPollInterval) { + logger.warn( + `Poll interval configuration is temporarily increased after Elasticsearch returned ${errorCount} "too many request" error(s).` + ); + } + } + return newPollInterval; + }, startingPollInterval); +} + +function countErrors(errors$: Observable, countInterval: number): Observable { + return merge( + // Flush error count at fixed interval + interval(countInterval).pipe(map(() => FLUSH_MARKER)), + errors$.pipe(filter((e) => SavedObjectsErrorHelpers.isTooManyRequestsError(e))) + ).pipe( + // When tag is "flush", reset the error counter + // Otherwise increment the error counter + mergeScan(({ count }, next) => { + return next === FLUSH_MARKER + ? of(emitErrorCount(count), resetErrorCount()) + : of(incementErrorCount(count)); + }, emitErrorCount(0)), + filter(isEmitEvent), + map(({ count }) => count) + ); +} + +function emitErrorCount(count: number) { + return { + tag: 'emit', + count, + }; +} + +function isEmitEvent(event: { tag: string; count: number }) { + return event.tag === 'emit'; +} + +function incementErrorCount(count: number) { + return { + tag: 'inc', + count: count + 1, + }; +} + +function resetErrorCount() { + return { + tag: 'initial', + count: 0, + }; +} diff --git a/x-pack/plugins/task_manager/server/polling/observable_monitor.ts b/x-pack/plugins/task_manager/server/polling/observable_monitor.ts index 7b06117ef59d1..b07bb6661163b 100644 --- a/x-pack/plugins/task_manager/server/polling/observable_monitor.ts +++ b/x-pack/plugins/task_manager/server/polling/observable_monitor.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subject, Observable, throwError, interval, timer, Subscription } from 'rxjs'; -import { exhaustMap, tap, takeUntil, switchMap, switchMapTo, catchError } from 'rxjs/operators'; +import { Subject, Observable, throwError, timer, Subscription } from 'rxjs'; import { noop } from 'lodash'; +import { exhaustMap, tap, takeUntil, switchMap, switchMapTo, catchError } from 'rxjs/operators'; const DEFAULT_HEARTBEAT_INTERVAL = 1000; @@ -29,7 +29,7 @@ export function createObservableMonitor( }: ObservableMonitorOptions = {} ): Observable { return new Observable((subscriber) => { - const subscription: Subscription = interval(heartbeatInterval) + const subscription: Subscription = timer(0, heartbeatInterval) .pipe( // switch from the heartbeat interval to the instantiated observable until it completes / errors exhaustMap(() => takeUntilDurationOfInactivity(observableFactory(), inactivityTimeout)), diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts index 607e2ac2b80fa..956c8b05f3860 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts @@ -5,11 +5,11 @@ */ import _ from 'lodash'; -import { Subject } from 'rxjs'; +import { Subject, of, BehaviorSubject } from 'rxjs'; import { Option, none, some } from 'fp-ts/lib/Option'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { fakeSchedulers } from 'rxjs-marbles/jest'; -import { sleep, resolvable, Resolvable } from '../test_utils'; +import { sleep, resolvable, Resolvable, mockLogger } from '../test_utils'; import { asOk, asErr } from '../lib/result_type'; describe('TaskPoller', () => { @@ -24,10 +24,12 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, getCapacity: () => 1, work, + workTimeout: pollInterval * 5, pollRequests$: new Subject>(), }).subscribe(() => {}); @@ -40,9 +42,52 @@ describe('TaskPoller', () => { await sleep(0); expect(work).toHaveBeenCalledTimes(1); + await sleep(0); + await sleep(0); + advance(pollInterval + 10); + await sleep(0); + expect(work).toHaveBeenCalledTimes(2); + }) + ); + + test( + 'poller adapts to pollInterval changes', + fakeSchedulers(async (advance) => { + const pollInterval = 100; + const pollInterval$ = new BehaviorSubject(pollInterval); + const bufferCapacity = 5; + + const work = jest.fn(async () => true); + createTaskPoller({ + logger: mockLogger(), + pollInterval$, + bufferCapacity, + getCapacity: () => 1, + work, + workTimeout: pollInterval * 5, + pollRequests$: new Subject>(), + }).subscribe(() => {}); + + // `work` is async, we have to force a node `tick` await sleep(0); advance(pollInterval); + expect(work).toHaveBeenCalledTimes(1); + + pollInterval$.next(pollInterval * 2); + + // `work` is async, we have to force a node `tick` + await sleep(0); + advance(pollInterval); + expect(work).toHaveBeenCalledTimes(1); + advance(pollInterval); expect(work).toHaveBeenCalledTimes(2); + + pollInterval$.next(pollInterval / 2); + + // `work` is async, we have to force a node `tick` + await sleep(0); + advance(pollInterval / 2); + expect(work).toHaveBeenCalledTimes(3); }) ); @@ -56,9 +101,11 @@ describe('TaskPoller', () => { let hasCapacity = true; createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => (hasCapacity ? 1 : 0), pollRequests$: new Subject>(), }).subscribe(() => {}); @@ -113,9 +160,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 1, pollRequests$, }).subscribe(jest.fn()); @@ -157,9 +206,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => (hasCapacity ? 1 : 0), pollRequests$, }).subscribe(() => {}); @@ -200,9 +251,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 1, pollRequests$, }).subscribe(() => {}); @@ -235,7 +288,8 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { await worker; @@ -285,7 +339,8 @@ describe('TaskPoller', () => { type ResolvableTupple = [string, PromiseLike & Resolvable]; const pollRequests$ = new Subject>(); createTaskPoller<[string, Resolvable], string[]>({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...resolvables) => { await Promise.all(resolvables.map(([, future]) => future)); @@ -344,11 +399,13 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { throw new Error('failed to work'); }, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); @@ -383,9 +440,11 @@ describe('TaskPoller', () => { return callCount; }); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); @@ -424,9 +483,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => {}); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.ts b/x-pack/plugins/task_manager/server/polling/task_poller.ts index a1435ffafe8f8..7515668a19d40 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.ts @@ -11,10 +11,11 @@ import { performance } from 'perf_hooks'; import { after } from 'lodash'; import { Subject, merge, interval, of, Observable } from 'rxjs'; -import { mapTo, filter, scan, concatMap, tap, catchError } from 'rxjs/operators'; +import { mapTo, filter, scan, concatMap, tap, catchError, switchMap } from 'rxjs/operators'; import { pipe } from 'fp-ts/lib/pipeable'; import { Option, none, map as mapOptional, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../types'; import { pullFromSet } from '../lib/pull_from_set'; import { Result, @@ -30,12 +31,13 @@ import { timeoutPromiseAfter } from './timeout_promise_after'; type WorkFn = (...params: T[]) => Promise; interface Opts { - pollInterval: number; + logger: Logger; + pollInterval$: Observable; bufferCapacity: number; getCapacity: () => number; pollRequests$: Observable>; work: WorkFn; - workTimeout?: number; + workTimeout: number; } /** @@ -52,7 +54,8 @@ interface Opts { * of unique request argumets of type T. The queue holds all the buffered request arguments streamed in via pollRequests$ */ export function createTaskPoller({ - pollInterval, + logger, + pollInterval$, getCapacity, pollRequests$, bufferCapacity, @@ -67,7 +70,13 @@ export function createTaskPoller({ // emit a polling event on demand pollRequests$, // emit a polling event on a fixed interval - interval(pollInterval).pipe(mapTo(none)) + pollInterval$.pipe( + switchMap((period) => { + logger.debug(`Task poller now using interval of ${period}ms`); + return interval(period); + }), + mapTo(none) + ) ).pipe( // buffer all requests in a single set (to remove duplicates) as we don't want // work to take place in parallel (it could cause Task Manager to pull in the same @@ -95,7 +104,7 @@ export function createTaskPoller({ await promiseResult( timeoutPromiseAfter( work(...pullFromSet(set, getCapacity())), - workTimeout ?? pollInterval, + workTimeout, () => new Error(`work has timed out`) ) ), diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index fb2d5e07030a4..cc611e124ea7b 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -17,6 +17,7 @@ import { ISavedObjectsRepository, } from '../../../../src/core/server'; import { Result, asOk, asErr, either, map, mapErr, promiseResult } from './lib/result_type'; +import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskManagerConfig } from './config'; import { Logger } from './types'; @@ -149,6 +150,13 @@ export class TaskManager { // pipe store events into the TaskManager's event stream this.store.events.subscribe((event) => this.events$.next(event)); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + logger: this.logger, + errors$: this.store.errors$, + startingMaxWorkers: opts.config.max_workers, + startingPollInterval: opts.config.poll_interval, + }); + this.bufferedStore = new BufferedTaskStore(this.store, { bufferMaxOperations: opts.config.max_workers, logger: this.logger, @@ -156,7 +164,7 @@ export class TaskManager { this.pool = new TaskPool({ logger: this.logger, - maxWorkers: opts.config.max_workers, + maxWorkers$: maxWorkersConfiguration$, }); const { @@ -166,7 +174,8 @@ export class TaskManager { this.poller$ = createObservableMonitor>, Error>( () => createTaskPoller({ - pollInterval, + logger: this.logger, + pollInterval$: pollIntervalConfiguration$, bufferCapacity: opts.config.request_capacity, getCapacity: () => this.pool.availableWorkers, pollRequests$: this.claimRequests$, diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 8b2bce455589e..12b731b2b78ae 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -5,6 +5,7 @@ */ import sinon from 'sinon'; +import { of, Subject } from 'rxjs'; import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; @@ -14,7 +15,7 @@ import moment from 'moment'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { const pool = new TaskPool({ - maxWorkers: 200, + maxWorkers$: of(200), logger: mockLogger(), }); @@ -26,7 +27,7 @@ describe('TaskPool', () => { test('availableWorkers are a function of total_capacity - occupiedWorkers', async () => { const pool = new TaskPool({ - maxWorkers: 10, + maxWorkers$: of(10), logger: mockLogger(), }); @@ -36,9 +37,21 @@ describe('TaskPool', () => { expect(pool.availableWorkers).toEqual(7); }); + test('availableWorkers is 0 until maxWorkers$ pushes a value', async () => { + const maxWorkers$ = new Subject(); + const pool = new TaskPool({ + maxWorkers$, + logger: mockLogger(), + }); + + expect(pool.availableWorkers).toEqual(0); + maxWorkers$.next(10); + expect(pool.availableWorkers).toEqual(10); + }); + test('does not run tasks that are beyond its available capacity', async () => { const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger: mockLogger(), }); @@ -60,7 +73,7 @@ describe('TaskPool', () => { test('should log when marking a Task as running fails', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger, }); @@ -83,7 +96,7 @@ describe('TaskPool', () => { test('should log when running a Task fails', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 3, + maxWorkers$: of(3), logger, }); @@ -106,7 +119,7 @@ describe('TaskPool', () => { test('should not log when running a Task fails due to the Task SO having been deleted while in flight', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 3, + maxWorkers$: of(3), logger, }); @@ -117,11 +130,9 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); - expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Task TaskType \\"shooooo\\" failed in attempt to run: Saved object [task/foo] not found", - ] - `); + expect(logger.debug).toHaveBeenCalledWith( + 'Task TaskType "shooooo" failed in attempt to run: Saved object [task/foo] not found' + ); expect(logger.warn).not.toHaveBeenCalled(); expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); @@ -130,7 +141,7 @@ describe('TaskPool', () => { test('Running a task which fails still takes up capacity', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 1, + maxWorkers$: of(1), logger, }); @@ -147,7 +158,7 @@ describe('TaskPool', () => { test('clears up capacity when a task completes', async () => { const pool = new TaskPool({ - maxWorkers: 1, + maxWorkers$: of(1), logger: mockLogger(), }); @@ -193,7 +204,7 @@ describe('TaskPool', () => { test('run cancels expired tasks prior to running new tasks', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger, }); @@ -251,7 +262,7 @@ describe('TaskPool', () => { const logger = mockLogger(); const pool = new TaskPool({ logger, - maxWorkers: 20, + maxWorkers$: of(20), }); const cancelled = resolvable(); diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 92374908c60f7..44f5f5648c2ac 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -8,6 +8,7 @@ * This module contains the logic that ensures we don't run too many * tasks at once in a given Kibana instance. */ +import { Observable } from 'rxjs'; import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; import { padStart } from 'lodash'; @@ -16,7 +17,7 @@ import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; interface Opts { - maxWorkers: number; + maxWorkers$: Observable; logger: Logger; } @@ -31,7 +32,7 @@ const VERSION_CONFLICT_MESSAGE = 'Task has been claimed by another Kibana servic * Runs tasks in batches, taking costs into account. */ export class TaskPool { - private maxWorkers: number; + private maxWorkers: number = 0; private running = new Set(); private logger: Logger; @@ -44,8 +45,11 @@ export class TaskPool { * @prop {Logger} logger - The task manager logger. */ constructor(opts: Opts) { - this.maxWorkers = opts.maxWorkers; this.logger = opts.logger; + opts.maxWorkers$.subscribe((maxWorkers) => { + this.logger.debug(`Task pool now using ${maxWorkers} as the max worker value`); + this.maxWorkers = maxWorkers; + }); } /** diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index f5fafe83748d9..5a3ee12d593c9 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import uuid from 'uuid'; -import { filter, take } from 'rxjs/operators'; +import { filter, take, first } from 'rxjs/operators'; import { Option, some, none } from 'fp-ts/lib/Option'; import { @@ -66,8 +66,21 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); describe('TaskStore', () => { describe('schedule', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + async function testSchedule(task: unknown) { - const callCluster = jest.fn(); savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ id: 'testid', type, @@ -75,15 +88,6 @@ describe('TaskStore', () => { references: [], version: '123', })); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster, - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); const result = await store.schedule(task as TaskInstance); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -176,12 +180,28 @@ describe('TaskStore', () => { /Unsupported task type "nope"/i ); }); + + test('pushes error from saved objects client to errors$', async () => { + const task: TaskInstance = { + id: 'id', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.create.mockRejectedValue(new Error('Failure')); + await expect(store.schedule(task)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('fetch', () => { - async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { - const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ hits: { hits } })); - const store = new TaskStore({ + let store: TaskStore; + const callCluster = jest.fn(); + + beforeAll(() => { + store = new TaskStore({ index: 'tasky', taskManagerId: '', serializer, @@ -190,15 +210,19 @@ describe('TaskStore', () => { definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); + }); + + async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { + callCluster.mockResolvedValue({ hits: { hits } }); const result = await store.fetch(opts); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'search'); + expect(callCluster).toHaveBeenCalledTimes(1); + expect(callCluster).toHaveBeenCalledWith('search', expect.anything()); return { result, - args: callCluster.args[0][1], + args: callCluster.mock.calls[0][1], }; } @@ -230,6 +254,13 @@ describe('TaskStore', () => { }, }); }); + + test('pushes error from call cluster to errors$', async () => { + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + callCluster.mockRejectedValue(new Error('Failure')); + await expect(store.fetch()).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('claimAvailableTasks', () => { @@ -928,9 +959,46 @@ if (doc['task.runAt'].size()!=0) { }, ]); }); + + test('pushes error from saved objects client to errors$', async () => { + const callCluster = jest.fn(); + const store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster, + definitions: taskDefinitions, + maxAttempts: 2, + savedObjectsRepository: savedObjectsClient, + }); + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + callCluster.mockRejectedValue(new Error('Failure')); + await expect( + store.claimAvailableTasks({ + claimOwnershipUntil: new Date(), + size: 10, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('update', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + test('refreshes the index, handles versioning', async () => { const task = { runAt: mockedDate, @@ -959,16 +1027,6 @@ if (doc['task.runAt'].size()!=0) { } ); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster: jest.fn(), - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); - const result = await store.update(task); expect(savedObjectsClient.update).toHaveBeenCalledWith( @@ -1002,28 +1060,116 @@ if (doc['task.runAt'].size()!=0) { version: '123', }); }); + + test('pushes error from saved objects client to errors$', async () => { + const task = { + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + retryAt: null, + id: 'task:324242', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + attempts: 3, + status: 'idle' as TaskStatus, + version: '123', + ownerId: null, + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.update.mockRejectedValue(new Error('Failure')); + await expect(store.update(task)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); + }); + + describe('bulkUpdate', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + + test('pushes error from saved objects client to errors$', async () => { + const task = { + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + retryAt: null, + id: 'task:324242', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + attempts: 3, + status: 'idle' as TaskStatus, + version: '123', + ownerId: null, + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.bulkUpdate.mockRejectedValue(new Error('Failure')); + await expect(store.bulkUpdate([task])).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failure"` + ); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('remove', () => { - test('removes the task with the specified id', async () => { - const id = `id-${_.random(1, 20)}`; - const callCluster = jest.fn(); - const store = new TaskStore({ + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ index: 'tasky', taskManagerId: '', serializer, - callCluster, + callCluster: jest.fn(), maxAttempts: 2, definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); + }); + + test('removes the task with the specified id', async () => { + const id = `id-${_.random(1, 20)}`; const result = await store.remove(id); expect(result).toBeUndefined(); expect(savedObjectsClient.delete).toHaveBeenCalledWith('task', id); }); + + test('pushes error from saved objects client to errors$', async () => { + const id = `id-${_.random(1, 20)}`; + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.delete.mockRejectedValue(new Error('Failure')); + await expect(store.remove(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('get', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + test('gets the task with the specified id', async () => { const id = `id-${_.random(1, 20)}`; const task = { @@ -1041,7 +1187,6 @@ if (doc['task.runAt'].size()!=0) { ownerId: null, }; - const callCluster = jest.fn(); savedObjectsClient.get.mockImplementation(async (type: string, objectId: string) => ({ id: objectId, type, @@ -1053,22 +1198,20 @@ if (doc['task.runAt'].size()!=0) { version: '123', })); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster, - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); - const result = await store.get(id); expect(result).toEqual(task); expect(savedObjectsClient.get).toHaveBeenCalledWith('task', id); }); + + test('pushes error from saved objects client to errors$', async () => { + const id = `id-${_.random(1, 20)}`; + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.get.mockRejectedValue(new Error('Failure')); + await expect(store.get(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('getLifecycle', () => { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index acd19bd75f7a3..15261be3d89ae 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -121,6 +121,7 @@ export class TaskStore { public readonly maxAttempts: number; public readonly index: string; public readonly taskManagerId: string; + public readonly errors$ = new Subject(); private callCluster: ElasticJs; private definitions: TaskDictionary; @@ -171,11 +172,17 @@ export class TaskStore { ); } - const savedObject = await this.savedObjectsRepository.create( - 'task', - taskInstanceToAttributes(taskInstance), - { id: taskInstance.id, refresh: false } - ); + let savedObject; + try { + savedObject = await this.savedObjectsRepository.create( + 'task', + taskInstanceToAttributes(taskInstance), + { id: taskInstance.id, refresh: false } + ); + } catch (e) { + this.errors$.next(e); + throw e; + } return savedObjectToConcreteTaskInstance(savedObject); } @@ -333,12 +340,22 @@ export class TaskStore { */ public async update(doc: ConcreteTaskInstance): Promise { const attributes = taskInstanceToAttributes(doc); - const updatedSavedObject = await this.savedObjectsRepository.update< - SerializedConcreteTaskInstance - >('task', doc.id, attributes, { - refresh: false, - version: doc.version, - }); + + let updatedSavedObject; + try { + updatedSavedObject = await this.savedObjectsRepository.update( + 'task', + doc.id, + attributes, + { + refresh: false, + version: doc.version, + } + ); + } catch (e) { + this.errors$.next(e); + throw e; + } return savedObjectToConcreteTaskInstance( // The SavedObjects update api forces a Partial on the `attributes` on the response, @@ -362,8 +379,11 @@ export class TaskStore { return attrsById; }, new Map()); - const updatedSavedObjects: Array = ( - await this.savedObjectsRepository.bulkUpdate( + let updatedSavedObjects: Array; + try { + ({ saved_objects: updatedSavedObjects } = await this.savedObjectsRepository.bulkUpdate< + SerializedConcreteTaskInstance + >( docs.map((doc) => ({ type: 'task', id: doc.id, @@ -373,8 +393,11 @@ export class TaskStore { { refresh: false, } - ) - ).saved_objects; + )); + } catch (e) { + this.errors$.next(e); + throw e; + } return updatedSavedObjects.map((updatedSavedObject, index) => isSavedObjectsUpdateResponse(updatedSavedObject) @@ -404,7 +427,12 @@ export class TaskStore { * @returns {Promise} */ public async remove(id: string): Promise { - await this.savedObjectsRepository.delete('task', id); + try { + await this.savedObjectsRepository.delete('task', id); + } catch (e) { + this.errors$.next(e); + throw e; + } } /** @@ -414,7 +442,14 @@ export class TaskStore { * @returns {Promise} */ public async get(id: string): Promise { - return savedObjectToConcreteTaskInstance(await this.savedObjectsRepository.get('task', id)); + let result; + try { + result = await this.savedObjectsRepository.get('task', id); + } catch (e) { + this.errors$.next(e); + throw e; + } + return savedObjectToConcreteTaskInstance(result); } /** @@ -438,14 +473,20 @@ export class TaskStore { private async search(opts: SearchOpts = {}): Promise { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); - const result = await this.callCluster('search', { - index: this.index, - ignoreUnavailable: true, - body: { - ...opts, - query, - }, - }); + let result; + try { + result = await this.callCluster('search', { + index: this.index, + ignoreUnavailable: true, + body: { + ...opts, + query, + }, + }); + } catch (e) { + this.errors$.next(e); + throw e; + } const rawDocs = (result as SearchResponse).hits.hits; @@ -464,17 +505,23 @@ export class TaskStore { { max_docs }: UpdateByQueryOpts = {} ): Promise { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); - const result = await this.callCluster('updateByQuery', { - index: this.index, - ignoreUnavailable: true, - refresh: true, - max_docs, - conflicts: 'proceed', - body: { - ...opts, - query, - }, - }); + let result; + try { + result = await this.callCluster('updateByQuery', { + index: this.index, + ignoreUnavailable: true, + refresh: true, + max_docs, + conflicts: 'proceed', + body: { + ...opts, + query, + }, + }); + } catch (e) { + this.errors$.next(e); + throw e; + } // eslint-disable-next-line @typescript-eslint/naming-convention const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse; 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 478c7e42a0c16..396a06205eaa9 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1006,930 +1006,6 @@ } } } - }, - "otlp": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/cpp": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/erlang": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/go": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/java": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/php": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/python": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } - }, - "opentelemetry/webjs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "language": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "runtime": { - "properties": { - "name": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "version": { - "type": "array", - "items": { - "type": "keyword" - } - }, - "composite": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } - } - } } } }, diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index 4ec12a27e1b15..d5da9377ed870 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -8,7 +8,8 @@ "home", "licensing", "management", - "features" + "features", + "savedObjects" ], "optionalPlugins": [ "security", @@ -20,7 +21,6 @@ "discover", "kibanaUtils", "kibanaReact", - "savedObjects", "ml" ] } diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index a23465495aceb..44a29e78c048a 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -6,6 +6,7 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import { ScopedHistory } from 'kibana/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -25,6 +26,7 @@ export interface AppDependencies { storage: Storage; overlays: CoreStart['overlays']; history: ScopedHistory; + savedObjectsPlugin: SavedObjectsStart; ml: GetMlSharedImportsReturnType; } diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index feff17b813112..7e0774bb2198c 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -28,10 +28,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const savedObjectsClient = appDeps.savedObjects.client; const savedSearches = createSavedSearchesLoader({ savedObjectsClient, - indexPatterns, - search: appDeps.data.search, - chrome: appDeps.chrome, - overlays: appDeps.overlays, + savedObjects: appDeps.savedObjectsPlugin, }); const [searchItems, setSearchItems] = useState(undefined); diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 17db745652dbf..0de4a2ce75077 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -48,6 +48,7 @@ export async function mountManagementSection( storage: localStorage, uiSettings, history, + savedObjectsPlugin: plugins.savedObjects, ml: await getMlSharedImports(), }; diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index 74256a478e732..597bfe36a0038 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n as kbnI18n } from '@kbn/i18n'; import { CoreSetup } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { registerFeature } from './register_feature'; @@ -15,6 +16,7 @@ export interface PluginsDependencies { data: DataPublicPluginStart; management: ManagementSetup; home: HomePublicPluginSetup; + savedObjects: SavedObjectsStart; } export class TransformUiPlugin { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 7b81298e8e4b6..84726bc950ef2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -27,6 +27,8 @@ import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; +import { ConfirmAlertSave } from './confirm_alert_save'; +import { hasShowActionsCapability } from '../../lib/capabilities'; interface AlertAddProps { consumer: string; @@ -59,6 +61,7 @@ export const AlertAdd = ({ const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); + const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); const setAlert = (value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); @@ -74,8 +77,11 @@ export const AlertAdd = ({ alertTypeRegistry, actionTypeRegistry, docLinks, + capabilities, } = useAlertsContext(); + const canShowActions = hasShowActionsCapability(capabilities); + useEffect(() => { setAlertProperty('alertTypeId', alertTypeId); }, [alertTypeId]); @@ -85,6 +91,17 @@ export const AlertAdd = ({ setAlert(initialAlert); }, [initialAlert, setAddFlyoutVisibility]); + const saveAlertAndCloseFlyout = async () => { + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } + } + }; + if (!addFlyoutVisible) { return null; } @@ -109,6 +126,9 @@ export const AlertAdd = ({ !!Object.keys(errorObj.errors).find((errorKey) => errorObj.errors[errorKey].length >= 1) ) !== undefined; + // Confirm before saving if user is able to add actions but hasn't added any to this alert + const shouldConfirmSave = canShowActions && alert.actions?.length === 0; + async function onSaveAlert(): Promise { try { const newAlert = await createAlert({ http, alert }); @@ -195,13 +215,10 @@ export const AlertAdd = ({ isLoading={isSaving} onClick={async () => { setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); - } + if (shouldConfirmSave) { + setIsConfirmAlertSaveModalOpen(true); + } else { + await saveAlertAndCloseFlyout(); } }} > @@ -214,6 +231,18 @@ export const AlertAdd = ({ + {isConfirmAlertSaveModalOpen && ( + { + setIsConfirmAlertSaveModalOpen(false); + await saveAlertAndCloseFlyout(); + }} + onCancel={() => { + setIsSaving(false); + setIsConfirmAlertSaveModalOpen(false); + }} + /> + )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx new file mode 100644 index 0000000000000..f23948d1d81bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + onConfirm: () => void; + onCancel: () => void; +} + +export const ConfirmAlertSave: React.FC = ({ onConfirm, onCancel }) => { + return ( + + +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 067972a452f27..bebc232b968d9 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -93,7 +93,6 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ panels = [ { id: ALERT_CONTEXT_MAIN_PANEL_ID, - title: 'main panel', items: [...selectionItems, managementContextItem], }, ]; @@ -101,7 +100,6 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ panels = [ { id: ALERT_CONTEXT_MAIN_PANEL_ID, - title: 'main panel', items: [ { 'aria-label': ToggleFlyoutTranslations.openAlertContextPanelAriaLabel, @@ -140,6 +138,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ closePopover={() => setIsOpen(false)} isOpen={isOpen} ownFocus + panelPaddingSize="none" > diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts index 544b976bb5ffa..2142e5ea1e2f6 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts @@ -5,7 +5,7 @@ */ import { KibanaTelemetryAdapter } from '../kibana_telemetry_adapter'; - +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; jest .spyOn(KibanaTelemetryAdapter, 'countNoOfUniqueMonitorAndLocations') .mockResolvedValue(undefined as any); @@ -13,7 +13,12 @@ jest describe('KibanaTelemetryAdapter', () => { let usageCollection: any; let getSavedObjectsClient: any; - let collector: { type: string; fetch: () => Promise; isReady: () => boolean }; + let collectorFetchContext: any; + let collector: { + type: string; + fetch: (collectorFetchParams: any) => Promise; + isReady: () => boolean; + }; beforeEach(() => { usageCollection = { makeUsageCollector: (val: any) => { @@ -23,6 +28,7 @@ describe('KibanaTelemetryAdapter', () => { getSavedObjectsClient = () => { return {}; }; + collectorFetchContext = createCollectorFetchContextMock(); }); it('collects monitor and overview data', async () => { @@ -49,7 +55,7 @@ describe('KibanaTelemetryAdapter', () => { autoRefreshEnabled: true, autorefreshInterval: 30, }); - const result = await collector.fetch(); + const result = await collector.fetch(collectorFetchContext); expect(result).toMatchSnapshot(); }); @@ -87,7 +93,7 @@ describe('KibanaTelemetryAdapter', () => { autoRefreshEnabled: true, autorefreshInterval: 30, }); - const result = await collector.fetch(); + const result = await collector.fetch(collectorFetchContext); expect(result).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index 106aab3515470..a8969f2621f29 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PageViewParams, UptimeTelemetry, Usage } from './types'; import { ESAPICaller } from '../framework'; import { savedObjectsAdapter } from '../../saved_objects'; @@ -69,7 +69,7 @@ export class KibanaTelemetryAdapter { }, }, }, - fetch: async (callCluster: ESAPICaller) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const savedObjectsClient = getSavedObjectsClient()!; if (savedObjectsClient) { await this.countNoOfUniqueMonitorAndLocations(callCluster, savedObjectsClient); diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts index 02fa669cb05e0..e4afbb0902830 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts @@ -13,14 +13,14 @@ import { ServiceStatus, ServiceStatusLevels, } from '../../../../../src/core/server'; -import { contextServiceMock } from '../../../../../src/core/server/mocks'; +import { contextServiceMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { createHttpServer } from '../../../../../src/core/server/test_utils'; import { registerSettingsRoute } from './settings'; type HttpService = ReturnType; type HttpSetup = UnwrapPromise>; -describe('/api/stats', () => { +describe('/api/settings', () => { let server: HttpService; let httpSetup: HttpSetup; let overallStatus$: BehaviorSubject; @@ -38,6 +38,9 @@ describe('/api/stats', () => { callAsCurrentUser: mockApiCaller, }, }, + client: { + asCurrentUser: elasticsearchServiceMock.createScopedClusterClient().asCurrentUser, + }, }, }, }), diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.ts index 2a0eb3d11584e..091344ae4e894 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -42,6 +42,10 @@ export function registerSettingsRoute({ }, async (context, req, res) => { const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; + const collectorFetchContext = { + callCluster: callAsCurrentUser, + esClient: context.core.elasticsearch.client.asCurrentUser, + }; const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE) as | KibanaSettingsCollector @@ -51,7 +55,7 @@ export function registerSettingsRoute({ } const settings = - (await settingsCollector.fetch(callAsCurrentUser)) ?? + (await settingsCollector.fetch(collectorFetchContext)) ?? settingsCollector.getEmailValueStructure(null); const { cluster_uuid: uuid } = await callAsCurrentUser('info', { filterPath: 'cluster_uuid', diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts new file mode 100644 index 0000000000000..3ffcf20c3399b --- /dev/null +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana overview', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('kibanaOverview'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.removeSampleDataSet('flights'); + await esArchiver.unload('empty_kibana'); + }); + + it('Getting started view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Overview view', async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('kibanaOverview'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 6b3a2a9add89f..8dace50a1ec87 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -25,6 +25,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/dashboard_edit_panel'), require.resolve('./apps/users'), require.resolve('./apps/roles'), + require.resolve('./apps/kibana_overview'), ], pageObjects, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index 2f57d05be4227..65e75f33072c3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -13,8 +13,6 @@ const NoKibanaPrivileges: User = { role: { name: 'no_kibana_privileges', elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: ['foo'], @@ -56,8 +54,6 @@ const GlobalRead: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -85,8 +81,6 @@ const Space1All: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -113,8 +107,6 @@ const Space1AllAlertingNoneActions: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -142,8 +134,6 @@ const Space1AllWithRestrictedFixture: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js index c859037597821..3de3a3279f77c 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js @@ -29,6 +29,7 @@ export default function ({ getService }) { const nodesIds = Object.keys(nodeStats.nodes); const { body } = await loadNodes().expect(200); + expect(body.isUsingDeprecatedDataRoleConfig).to.eql(false); expect(body.nodesByAttributes[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); }); }); diff --git a/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts index 2e38c5317c382..00c1ae12e182a 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts @@ -60,7 +60,9 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql([{ id: 'success_cardinality' }]); }); - it(`should recognize a high model plot cardinality`, async () => { + // Failing ES promotion due to changes in the cardinality agg, + // see https://github.com/elastic/kibana/issues/80418 + it.skip(`should recognize a high model plot cardinality`, async () => { const requestBody = { job_id: '', description: '', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 01a34f110ed1e..01e3da64a515d 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -178,7 +178,9 @@ export default ({ getService }: FtrProviderContext) => { ]); }); - it('should recognize non-basic issues in job configuration', async () => { + // Failing ES promotion due to changes in the cardinality agg, + // see https://github.com/elastic/kibana/issues/80418 + it.skip('should recognize non-basic issues in job configuration', async () => { const requestBody = { duration: { start: 1586995459000, end: 1589672736000 }, job: { diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 5db456ee26107..6179c88916639 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -14,6 +14,7 @@ export enum ApmUser { apmReadUser = 'apm_read_user', apmWriteUser = 'apm_write_user', apmAnnotationsWriteUser = 'apm_annotations_write_user', + apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access', } const roles = { @@ -27,6 +28,15 @@ const roles = { }, ], }, + [ApmUser.apmReadUserWithoutMlAccess]: { + kibana: [ + { + base: [], + feature: { apm: ['read'] }, + spaces: ['*'], + }, + ], + }, [ApmUser.apmWriteUser]: { kibana: [ { @@ -63,6 +73,9 @@ const users = { [ApmUser.apmReadUser]: { roles: ['apm_user', ApmUser.apmReadUser], }, + [ApmUser.apmReadUserWithoutMlAccess]: { + roles: ['apm_user', ApmUser.apmReadUserWithoutMlAccess], + }, [ApmUser.apmWriteUser]: { roles: ['apm_user', ApmUser.apmWriteUser], }, diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 5edf1bf23e594..db073cb967423 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -63,6 +63,10 @@ export function createTestConfig(settings: Settings) { servers.kibana, ApmUser.apmAnnotationsWriteUser ), + supertestAsApmReadUserWithoutMlAccess: supertestAsApmUser( + servers.kibana, + ApmUser.apmReadUserWithoutMlAccess + ), }, junit: { reportName: name, diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index 1249561a549bd..a7e6ae03b1bdc 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -1046,7 +1046,7 @@ Array [ ] `; -exports[`Service Maps with a trial license when there is data with anomalies returns the correct anomaly stats 3`] = ` +exports[`Service Maps with a trial license when there is data with anomalies with the default apm user returns the correct anomaly stats 3`] = ` Object { "elements": Array [ Object { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 6e7046ac0ba12..0cd91eb46a5e2 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -14,6 +14,8 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); + const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; @@ -128,34 +130,35 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); - let response: PromiseReturnType; + describe('with the default apm user', () => { + let response: PromiseReturnType; - before(async () => { - response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - }); + before(async () => { + response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + }); - it('returns service map elements with anomaly stats', () => { - expect(response.status).to.be(200); - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) - ); + it('returns service map elements with anomaly stats', () => { + expect(response.status).to.be(200); + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); - expect(dataWithAnomalies).to.not.empty(); + expect(dataWithAnomalies).to.not.empty(); - dataWithAnomalies.forEach(({ data }: any) => { - expect( - Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) - ).to.not.empty(); + dataWithAnomalies.forEach(({ data }: any) => { + expect( + Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) + ).to.not.empty(); + }); }); - }); - it('returns the correct anomaly stats', () => { - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) - ); + it('returns the correct anomaly stats', () => { + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); - expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`); - expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(` + expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`); + expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(` Array [ Object { "data": Object { @@ -203,7 +206,28 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ] `); - expectSnapshot(response.body).toMatch(); + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('with a user that does not have access to ML', () => { + let response: PromiseReturnType; + + before(async () => { + response = await supertestAsApmReadUserWithoutMlAccess.get( + `/api/apm/service-map?start=${start}&end=${end}` + ); + }); + + it('returns service map elements without anomaly stats', () => { + expect(response.status).to.be(200); + + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); + + expect(dataWithAnomalies).to.be.empty(); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index c23c26f504a6c..6fd5e7e0c3ea7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -12,6 +12,7 @@ import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; @@ -29,35 +30,36 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); - describe('and fetching a list of services', () => { - let response: PromiseReturnType; - before(async () => { - response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - }); + describe('with the default APM read user', () => { + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); - it('the response is successful', () => { - expect(response.status).to.eql(200); - }); + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); - it('there is at least one service', () => { - expect(response.body.items.length).to.be.greaterThan(0); - }); + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); - it('some items have a health status set', () => { - // Under the assumption that the loaded archive has - // at least one APM ML job, and the time range is longer - // than 15m, at least one items should have a health status - // set. Note that we currently have a bug where healthy - // services report as unknown (so without any health status): - // https://github.com/elastic/kibana/issues/77083 + it('some items have a health status set', () => { + // Under the assumption that the loaded archive has + // at least one APM ML job, and the time range is longer + // than 15m, at least one items should have a health status + // set. Note that we currently have a bug where healthy + // services report as unknown (so without any health status): + // https://github.com/elastic/kibana/issues/77083 - const healthStatuses = response.body.items.map((item: any) => item.healthStatus); + const healthStatuses = response.body.items.map((item: any) => item.healthStatus); - expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); + expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); - expectSnapshot(healthStatuses).toMatchInline(` + expectSnapshot(healthStatuses).toMatchInline(` Array [ "healthy", undefined, @@ -69,6 +71,32 @@ export default function ApiTest({ getService }: FtrProviderContext) { "healthy", ] `); + }); + }); + }); + + describe('with a user that does not have access to ML', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertestAsApmReadUserWithoutMlAccess.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); + + it('contains no health statuses', () => { + const definedHealthStatuses = response.body.items + .map((item: any) => item.healthStatus) + .filter(Boolean); + + expect(definedHealthStatuses.length).to.be(0); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts new file mode 100644 index 0000000000000..620e771b3446d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { SearchResponse } from 'elasticsearch'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getQueryAllSignals, + removeServerGeneratedProperties, + waitFor, +} from '../../utils'; + +import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + /** + * Specific api integration tests for threat matching rule type + */ + describe('create_threat_matching', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating threat match rule', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + }); + + it('should create a single rule with a rule_id and validate it ran successfully', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await deleteAllAlerts(es); + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute and get 10 signals when doing a specific query', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be return zero matches if the mapping does not match against anything in the mapping', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'invalid.mapping.value', // invalid mapping value + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create the threat match rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); +}; 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 779205377621d..cc0eb04075b77 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 @@ -15,6 +15,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./create_threat_matching')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index d26c92a2bcd63..6c4fa94a259e9 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/77969 - describe.skip('lens smokescreen tests', () => { + describe('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -153,6 +152,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', operation: 'max', field: 'memory', + keepOpen: true, }); await PageObjects.lens.editDimensionLabel('Test of label'); await PageObjects.lens.editDimensionFormat('Percent'); @@ -160,6 +160,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.editMissingValues('Linear'); await PageObjects.lens.assertMissingValues('Linear'); + + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); await PageObjects.lens.assertColor('#ff0000'); await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); diff --git a/x-pack/test/functional/es_archives/uptime/pings/data.json.gz b/x-pack/test/functional/es_archives/uptime/pings/data.json.gz index a8f7b84b7d0a8..83441218aad73 100644 Binary files a/x-pack/test/functional/es_archives/uptime/pings/data.json.gz and b/x-pack/test/functional/es_archives/uptime/pings/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index ec7281e53c5e1..f8ecacbc1141d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -114,6 +114,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont } }, + /** + * Open the specified dimension. + * + * @param dimension - the selector of the dimension panel to open + * @param layerIndex - the index of the layer + */ + async openDimensionEditor(dimension: string, layerIndex = 0) { + await retry.try(async () => { + await testSubjects.click(`lns-layerPanel-${layerIndex} > ${dimension}`); + }); + }, + // closes the dimension editor flyout async closeDimensionEditor() { await testSubjects.click('lns-indexPattern-dimensionContainerTitle'); diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index 3a2c9149a0634..3ab062dc2e6ee 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -48,12 +48,18 @@ export function MachineLearningSettingsCalendarProvider( return rows; }, - rowSelector(calendarId: string, subSelector?: string) { + calendarRowSelector(calendarId: string, subSelector?: string) { const row = `~mlCalendarTable > ~row-${calendarId}`; return !subSelector ? row : `${row} > ${subSelector}`; }, + async waitForCalendarTableToLoad() { + await testSubjects.existOrFail('~mlCalendarTable', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('mlCalendarTable loaded', { timeout: 30 * 1000 }); + }, + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + await this.waitForCalendarTableToLoad(); const tableListContainer = await testSubjects.find('mlCalendarTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); await searchBarInput.clearValueWithKeyboard(); @@ -69,7 +75,7 @@ export function MachineLearningSettingsCalendarProvider( async isCalendarRowSelected(calendarId: string): Promise { return await testSubjects.isChecked( - this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) ); }, @@ -85,7 +91,9 @@ export function MachineLearningSettingsCalendarProvider( async selectCalendarRow(calendarId: string) { if ((await this.isCalendarRowSelected(calendarId)) === false) { - await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + await testSubjects.click( + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); } await this.assertCalendarRowSelected(calendarId, true); @@ -93,7 +101,9 @@ export function MachineLearningSettingsCalendarProvider( async deselectCalendarRow(calendarId: string) { if ((await this.isCalendarRowSelected(calendarId)) === true) { - await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + await testSubjects.click( + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); } await this.assertCalendarRowSelected(calendarId, false); @@ -120,7 +130,7 @@ export function MachineLearningSettingsCalendarProvider( }, async openCalendarEditForm(calendarId: string) { - await testSubjects.click(this.rowSelector(calendarId, 'mlEditCalendarLink')); + await testSubjects.click(this.calendarRowSelector(calendarId, 'mlEditCalendarLink')); await testSubjects.existOrFail('mlPageCalendarEdit > mlCalendarFormEdit', { timeout: 5000 }); }, @@ -178,11 +188,6 @@ export function MachineLearningSettingsCalendarProvider( ); }, - calendarRowSelector(calendarId: string, subSelector?: string) { - const row = `~mlCalendarTable > ~row-${calendarId}`; - return !subSelector ? row : `${row} > ${subSelector}`; - }, - eventRowSelector(eventDescription: string, subSelector?: string) { const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -261,12 +266,20 @@ export function MachineLearningSettingsCalendarProvider( return isSelected === 'true'; }, + async assertApplyToAllJobsSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = this.getApplyToAllJobsSwitchCheckedState(); + expect(actualCheckState).to.eql( + expectedCheckState, + `Apply to all jobs switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` + ); + }, + async toggleApplyToAllJobsSwitch(toggle: boolean) { const subj = 'mlCalendarApplyToAllJobsSwitch'; if ((await this.getApplyToAllJobsSwitchCheckedState()) !== toggle) { await retry.tryForTime(5 * 1000, async () => { await testSubjects.clickWhenNotDisabled(subj); - await this.assertApplyToAllJobsSwitchEnabled(toggle); + await this.assertApplyToAllJobsSwitchCheckState(toggle); }); } }, diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index c4f75b843d781..6ade7dc485a88 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -114,5 +114,8 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { async clickSaveAlertButton() { return testSubjects.click('saveAlertButton'); }, + async clickSaveAlertsConfirmButton() { + return testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 25a7a57e52413..4dd7c9f3b3716 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -54,6 +54,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } + async function defineAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click('.index-threshold-SelectOption'); + await testSubjects.click('selectIndexExpression'); + const comboBox = await find.byCssSelector('#indexSelectSearchBox'); + await comboBox.click(); + await comboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + } + describe('alerts', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); @@ -62,25 +84,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click('.index-threshold-SelectOption'); - await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); - const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); - await filterSelectItem.click(); - await testSubjects.click('thresholdAlertTimeFieldSelect'); - await retry.try(async () => { - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - expect(fieldOptions[1]).not.to.be(undefined); - await fieldOptions[1].click(); - }); - await testSubjects.click('closePopover'); - // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('alertNameInput'); - await nameInput.click(); + await defineAlert(alertName); await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('addNewActionConnectorButton-.slack'); @@ -123,6 +127,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); }); + it('should show save confirmation before creating alert with no actions', async () => { + const alertName = generateUniqueKey(); + await defineAlert(alertName); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalCancelButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + await find.existsByCssSelector('[data-test-subj="saveAlertButton"]:not(disabled)'); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Saved '${alertName}'`); + await pageObjects.triggersActionsUI.searchAlerts(alertName); + const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); + expect(searchResultsAfterSave).to.eql([ + { + name: alertName, + tagsText: '', + alertType: 'Index threshold', + interval: '1m', + }, + ]); + + // clean up created alert + const alertsToDelete = await getAlertsByName(alertName); + await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + }); + it('should display alerts in alphabetical order', async () => { const uniqueKey = generateUniqueKey(); const a = await createAlert({ name: 'b', tags: [uniqueKey] }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index a6de87d6f7b1a..ff4ab65a310ed 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -79,6 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); @@ -165,6 +166,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts index 55ef7e9784ff4..c9512dd12b78e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts @@ -79,6 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index 5da9b5e3031b2..b9558240ca007 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -24,15 +24,16 @@ export default function (providerContext: FtrProviderContext) { await supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); }; const installPackage = async (pkg: string) => { - await supertest + return await supertest .post(`/api/fleet/epm/packages/${pkg}`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }; describe('datastreams', async () => { skipIfNoDockerRegistry(providerContext); - before(async () => { + beforeEach(async () => { await installPackage(pkgKey); await es.transport.request({ method: 'POST', @@ -61,8 +62,7 @@ export default function (providerContext: FtrProviderContext) { }, }); }); - after(async () => { - await uninstallPackage(pkgUpdateKey); + afterEach(async () => { await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, @@ -71,60 +71,57 @@ export default function (providerContext: FtrProviderContext) { method: 'DELETE', path: `/_data_stream/${metricsTemplateName}-default`, }); + await uninstallPackage(pkgKey); + await uninstallPackage(pkgUpdateKey); }); - describe('get datastreams after data sent', async () => { - skipIfNoDockerRegistry(providerContext); - let resLogsDatastream: any; - let resMetricsDatastream: any; - before(async () => { - resLogsDatastream = await es.transport.request({ - method: 'GET', - path: `/_data_stream/${logsTemplateName}-default`, - }); - resMetricsDatastream = await es.transport.request({ - method: 'GET', - path: `/_data_stream/${metricsTemplateName}-default`, - }); - }); - it('should list the logs datastream', async function () { - expect(resLogsDatastream.body.data_streams.length).equal(1); - expect(resLogsDatastream.body.data_streams[0].indices.length).equal(1); - expect(resLogsDatastream.body.data_streams[0].indices[0].index_name).equal( - `.ds-${logsTemplateName}-default-000001` - ); + it('should list the logs and metrics datastream', async function () { + const resLogsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${logsTemplateName}-default`, }); - it('should list the metrics datastream', async function () { - expect(resMetricsDatastream.body.data_streams.length).equal(1); - expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); - expect(resMetricsDatastream.body.data_streams[0].indices[0].index_name).equal( - `.ds-${metricsTemplateName}-default-000001` - ); + const resMetricsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${metricsTemplateName}-default`, }); + expect(resLogsDatastream.body.data_streams.length).equal(1); + expect(resLogsDatastream.body.data_streams[0].indices.length).equal(1); + expect(resLogsDatastream.body.data_streams[0].indices[0].index_name).equal( + `.ds-${logsTemplateName}-default-000001` + ); + expect(resMetricsDatastream.body.data_streams.length).equal(1); + expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); + expect(resMetricsDatastream.body.data_streams[0].indices[0].index_name).equal( + `.ds-${metricsTemplateName}-default-000001` + ); }); - describe('rollover datastream when mappings are not compatible', async () => { - skipIfNoDockerRegistry(providerContext); - let resLogsDatastream: any; - let resMetricsDatastream: any; - before(async () => { - await installPackage(pkgUpdateKey); - resLogsDatastream = await es.transport.request({ - method: 'GET', - path: `/_data_stream/${logsTemplateName}-default`, - }); - resMetricsDatastream = await es.transport.request({ - method: 'GET', - path: `/_data_stream/${metricsTemplateName}-default`, - }); + + it('after update, it should have rolled over logs datastream because mappings are not compatible and not metrics', async function () { + await installPackage(pkgUpdateKey); + const resLogsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${logsTemplateName}-default`, }); - it('should have rolled over logs datastream', async function () { - expect(resLogsDatastream.body.data_streams[0].indices.length).equal(2); - expect(resLogsDatastream.body.data_streams[0].indices[1].index_name).equal( - `.ds-${logsTemplateName}-default-000002` - ); + const resMetricsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${metricsTemplateName}-default`, + }); + expect(resLogsDatastream.body.data_streams[0].indices.length).equal(2); + expect(resLogsDatastream.body.data_streams[0].indices[1].index_name).equal( + `.ds-${logsTemplateName}-default-000002` + ); + expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); + }); + it('should be able to upgrade a package after a rollover', async function () { + await es.transport.request({ + method: 'POST', + path: `/${logsTemplateName}-default/_rollover`, }); - it('should have not rolled over metrics datastream', async function () { - expect(resMetricsDatastream.body.data_streams[0].indices.length).equal(1); + const resLogsDatastream = await es.transport.request({ + method: 'GET', + path: `/_data_stream/${logsTemplateName}-default`, }); + expect(resLogsDatastream.body.data_streams[0].indices.length).equal(2); + await installPackage(pkgUpdateKey); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 16a500ddf85ee..cc6a384dcaafe 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -35,6 +35,9 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await installPackage(pkgKey); }); + after(async () => { + await uninstallPackage(pkgKey); + }); it('should have installed the ILM policy', async function () { const resPolicy = await es.transport.request({ method: 'GET', @@ -218,6 +221,9 @@ export default function (providerContext: FtrProviderContext) { describe('uninstalls all assets when uninstalling a package', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { + // these tests ensure that uninstall works properly so make sure that the package gets installed and uninstalled + // and then we'll test that not artifacts are left behind. + await installPackage(pkgKey); await uninstallPackage(pkgKey); }); it('should have uninstalled the index templates', async function () { @@ -342,6 +348,48 @@ export default function (providerContext: FtrProviderContext) { } expect(resSearch.response.data.statusCode).equal(404); }); + it('should have removed the fields from the index patterns', async () => { + // The reason there is an expect inside the try and inside the catch in this test case is to guard against two + // different scenarios. + // + // If a test case in another file calls /setup then the system and endpoint packages will be installed and + // will be present for the remainder of the tests (because they cannot be removed). If that is the case the + // expect in the try will work because the logs-* and metrics-* index patterns will still be present even + // after this test uninstalls its package. + // + // If /setup was never called prior to this test, when the test package is uninstalled the index pattern code + // checks to see if there are no packages installed and completely removes the logs-* and metrics-* index + // patterns. If that happens this code will throw an error and indicate that the index pattern being searched + // for was completely removed. In this case the catch's expect will test to make sure the error thrown was + // a 404 because all of the packages have been removed. + try { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); + expect(exists).to.be(undefined); + } catch (err) { + // if all packages are uninstalled there won't be a logs-* index pattern + expect(err.response.data.statusCode).equal(404); + } + + try { + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + const existsMetrics = fieldsMetrics.find( + (field: { name: string }) => field.name === 'metrics_test_name' + ); + expect(existsMetrics).to.be(undefined); + } catch (err) { + // if all packages are uninstalled there won't be a metrics-* index pattern + expect(err.response.data.statusCode).equal(404); + } + }); it('should have removed the saved object', async function () { let res; try { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts index 090279eaa7c30..055877c19c82f 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts @@ -121,9 +121,24 @@ export default function (providerContext: FtrProviderContext) { expect(res.body.message).to.equal('agent agent1 is not upgradeable'); }); - it('should respond 200 to bulk upgrade agents and update the agent SOs', async () => { + it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => { const kibanaVersion = await kibanaServer.version.get(); - + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -138,11 +153,27 @@ export default function (providerContext: FtrProviderContext) { supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), ]); expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - it('should allow to upgrade multiple agents by kuery', async () => { + it('should allow to upgrade multiple upgradeable agents by kuery', async () => { const kibanaVersion = await kibanaServer.version.get(); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -156,7 +187,7 @@ export default function (providerContext: FtrProviderContext) { supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), ]); expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); it('should not upgrade an unenrolling agent during bulk_upgrade', async () => { @@ -164,6 +195,22 @@ export default function (providerContext: FtrProviderContext) { await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ force: true, }); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: '0.0.0' } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -180,10 +227,22 @@ export default function (providerContext: FtrProviderContext) { }); it('should not upgrade an unenrolled agent during bulk_upgrade', async () => { const kibanaVersion = await kibanaServer.version.get(); - kibanaServer.savedObjects.update({ + await kibanaServer.savedObjects.update({ id: 'agent1', type: AGENT_SAVED_OBJECT_TYPE, - attributes: { unenrolled_at: new Date().toISOString() }, + attributes: { + unenrolled_at: new Date().toISOString(), + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: '0.0.0' } }, + }, + }, }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) @@ -199,5 +258,46 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); + it('should not upgrade an non upgradeable agent during bulk_upgrade', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent3', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2', 'agent3'], + version: kibanaVersion, + }); + const [agent1data, agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined'); + }); }); } diff --git a/x-pack/test/reporting_api_integration/fixtures.ts b/x-pack/test/reporting_api_integration/fixtures.ts index 72d3ab9092a1d..c3448dada3a53 100644 --- a/x-pack/test/reporting_api_integration/fixtures.ts +++ b/x-pack/test/reporting_api_integration/fixtures.ts @@ -245,6 +245,20 @@ export const CSV_RESULT_NANOS_CUSTOM = `date,message,"_id" "Jan 1, 2015 @ 07:10:30.000000000","Hello 1", `; +export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" +`; + // This concatenates lines of multi-line string into a single line. // It is so long strings can be entered at short widths, making syntax highlighting easier on editors function singleLine(literals: TemplateStringsArray): string { @@ -261,16 +275,22 @@ format:strict_date_optional_time,gte:'2004-09-17T21:19:34.213Z',lte:'2019-09-17T :desc,unmapped_type:boolean))),stored_fields:!('@timestamp',clientip,extension),version:! t),index:'logstash-*'),title:'A Saved Search With a DATE FILTER',type:search)`; -export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" -`; +export const JOB_PARAMS_ECOM_MARKDOWN = singleLine`(browserTimezone:UTC,layout:(dimensions:(height:354.6000061035156,width:768),id:png),objectType:visualization,relativeUrl:\' + /app/visualize#/edit/4a36acd0-7ac3-11ea-b69c-cf0d7935cd67?_g=(filters:\u0021\u0021(),refreshInterval:(pause:\u0021\u0021t,value:0),time:(from:now-15m,to:no + w))&_a=(filters:\u0021\u0021(),linked:\u0021\u0021f,query:(language:kuery,query:\u0021\'\u0021\'),uiState:(),vis:(aggs:\u0021\u0021(),params:(fontSize:12,ma + rkdown:\u0021\'Ti%E1%BB%83u%20thuy%E1%BA%BFt%20l%C3%A0%20m%E1%BB%99t%20th%E1%BB%83%20lo%E1%BA%A1i%20v%C4%83n%20xu%C3%B4i%20c%C3%B3%20h%C6%B0%20c%E1%BA%A5u,% + 20th%C3%B4ng%20qua%20nh%C3%A2n%20v%E1%BA%ADt,%20ho%C3%A0n%20c%E1%BA%A3nh,%20s%E1%BB%B1%20vi%E1%BB%87c%20%C4%91%E1%BB%83%20ph%E1%BA%A3n%20%C3%A1nh%20b%E1%BB% + A9c%20tranh%20x%C3%A3%20h%E1%BB%99i%20r%E1%BB%99ng%20l%E1%BB%9Bn%20v%C3%A0%20nh%E1%BB%AFng%20v%E1%BA%A5n%20%C4%91%E1%BB%81%20c%E1%BB%A7a%20cu%E1%BB%99c%20s% + E1%BB%91ng%20con%20ng%C6%B0%E1%BB%9Di,%20bi%E1%BB%83u%20hi%E1%BB%87n%20t%C3%ADnh%20ch%E1%BA%A5t%20t%C6%B0%E1%BB%9Dng%20thu%E1%BA%ADt,%20t%C3%ADnh%20ch%E1%BA + %A5t%20k%E1%BB%83%20chuy%E1%BB%87n%20b%E1%BA%B1ng%20ng%C3%B4n%20ng%E1%BB%AF%20v%C4%83n%20xu%C3%B4i%20theo%20nh%E1%BB%AFng%20ch%E1%BB%A7%20%C4%91%E1%BB%81%20 + x%C3%A1c%20%C4%91%E1%BB%8Bnh.%0A%0ATrong%20m%E1%BB%99t%20c%C3%A1ch%20hi%E1%BB%83u%20kh%C3%A1c,%20nh%E1%BA%ADn%20%C4%91%E1%BB%8Bnh%20c%E1%BB%A7a%20Belinski:% + 20%22ti%E1%BB%83u%20thuy%E1%BA%BFt%20l%C3%A0%20s%E1%BB%AD%20thi%20c%E1%BB%A7a%20%C4%91%E1%BB%9Di%20t%C6%B0%22%20ch%E1%BB%89%20ra%20kh%C3%A1i%20qu%C3%A1t%20n + h%E1%BA%A5t%20v%E1%BB%81%20m%E1%BB%99t%20d%E1%BA%A1ng%20th%E1%BB%A9c%20t%E1%BB%B1%20s%E1%BB%B1,%20trong%20%C4%91%C3%B3%20s%E1%BB%B1%20tr%E1%BA%A7n%20thu%E1% + BA%ADt%20t%E1%BA%ADp%20trung%20v%C3%A0o%20s%E1%BB%91%20ph%E1%BA%ADn%20c%E1%BB%A7a%20m%E1%BB%99t%20c%C3%A1%20nh%C3%A2n%20trong%20qu%C3%A1%20tr%C3%ACnh%20h%C3 + %ACnh%20th%C3%A0nh%20v%C3%A0%20ph%C3%A1t%20tri%E1%BB%83n%20c%E1%BB%A7a%20n%C3%B3.%20S%E1%BB%B1%20tr%E1%BA%A7n%20thu%E1%BA%ADt%20%E1%BB%9F%20%C4%91%C3%A2y%20 + %C4%91%C6%B0%E1%BB%A3c%20khai%20tri%E1%BB%83n%20trong%20kh%C3%B4ng%20gian%20v%C3%A0%20th%E1%BB%9Di%20gian%20ngh%E1%BB%87%20thu%E1%BA%ADt%20%C4%91%E1%BA%BFn% + 20m%E1%BB%A9c%20%C4%91%E1%BB%A7%20%C4%91%E1%BB%83%20truy%E1%BB%81n%20%C4%91%E1%BA%A1t%20c%C6%A1%20c%E1%BA%A5u%20c%E1%BB%A7a%20nh%C3%A2n%20c%C3%A1ch%5B1%5D.% + 0A%0A%0A%5B1%5D%5E%20M%E1%BB%A5c%20t%E1%BB%AB%20Ti%E1%BB%83u%20thuy%E1%BA%BFt%20trong%20cu%E1%BB%91n%20150%20thu%E1%BA%ADt%20ng%E1%BB%AF%20v%C4%83n%20h%E1%B + B%8Dc,%20L%E1%BA%A1i%20Nguy%C3%AAn%20%C3%82n%20bi%C3%AAn%20so%E1%BA%A1n,%20Nh%C3%A0%20xu%E1%BA%A5t%20b%E1%BA%A3n%20%C4%90%E1%BA%A1i%20h%E1%BB%8Dc%20Qu%E1%BB + %91c%20gia%20H%C3%A0%20N%E1%BB%99i,%20in%20l%E1%BA%A7n%20th%E1%BB%A9%202%20c%C3%B3%20s%E1%BB%ADa%20%C4%91%E1%BB%95i%20b%E1%BB%95%20sung.%20H.%202003.%20Tran + g%20326.\u0021\',openLinksInNewTab:\u0021\u0021f),title:\u0021\'Ti%E1%BB%83u%20thuy%E1%BA%BFt\u0021\',type:markdown))\',title:\'Tiểu thuyết\')`; diff --git a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts index e3add3748f56d..e1999b71c662c 100644 --- a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ */ import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; - +import { pageObjects } from '../functional/page_objects'; // Reporting APIs depend on UI functionality import { services } from './services'; -export type FtrProviderContext = GenericFtrProviderContext; +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index b040040fc5114..4a95a15169b59 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -7,19 +7,19 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { format as formatUrl } from 'url'; -import { ReportingAPIProvider } from './services'; +import { pageObjects } from '../functional/page_objects'; // Reporting APIs depend on UI functionality +import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); return { + apps: { reporting: { pathname: '/app/management/insightsAndAlerting/reporting' } }, servers: apiConfig.get('servers'), junit: { reportName: 'X-Pack Reporting Without Security API Integration Tests' }, testFiles: [require.resolve('./reporting_without_security')], - services: { - ...apiConfig.get('services'), - reportingAPI: ReportingAPIProvider, - }, + services, + pageObjects, esArchiver: apiConfig.get('esArchiver'), esTestCluster: { ...apiConfig.get('esTestCluster'), diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 09351a2c9907a..12b32f0f6c4c6 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting APIs', function () { this.tags('ciGroup2'); loadTestFile(require.resolve('./job_apis')); + loadTestFile(require.resolve('./management')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/management.ts b/x-pack/test/reporting_api_integration/reporting_without_security/management.ts new file mode 100644 index 0000000000000..97eebf2b44502 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_without_security/management.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { JOB_PARAMS_ECOM_MARKDOWN } from '../fixtures'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['common', 'reporting']); + const log = getService('log'); + const supertest = getService('supertestWithoutAuth'); + + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const reportingApi = getService('reportingAPI'); + + const postJobJSON = async ( + apiPath: string, + jobJSON: object = {} + ): Promise<{ path: string; status: number }> => { + log.debug(`postJobJSON((${apiPath}): ${JSON.stringify(jobJSON)})`); + const { body, status } = await supertest.post(apiPath).set('kbn-xsrf', 'xxx').send(jobJSON); + return { status, path: body.path }; + }; + + describe('Polling for jobs', () => { + beforeEach(async () => { + await esArchiver.load('empty_kibana'); + await esArchiver.load('reporting/ecommerce_kibana'); + }); + + afterEach(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.unload('reporting/ecommerce_kibana'); + await reportingApi.deleteAllReports(); + }); + + it('Displays new jobs', async () => { + await PageObjects.common.navigateToApp('reporting'); + await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + + // post new job + const { status } = await postJobJSON(`/api/reporting/generate/png`, { + jobParams: JOB_PARAMS_ECOM_MARKDOWN, + }); + expect(status).to.be(200); + + await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs + + const tableElem = await testSubjects.find('reportJobListing'); + const tableRow = await tableElem.findByCssSelector('tbody tr td+td'); // find the title cell of the first row + const tableCellText = await tableRow.getVisibleText(); + expect(tableCellText).to.be(`Tiểu thuyết\nvisualization`); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 654aa18fba523..3e3aeee305433 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -32,5 +32,6 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./endpoint_telemetry')); + loadTestFile(require.resolve('./trusted_apps_list')); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 5f749ac272474..78ef1bc894e0b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -8,22 +8,46 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['trustedApps']); + const pageObjects = getPageObjects(['common', 'trustedApps']); const testSubjects = getService('testSubjects'); - describe('endpoint list', function () { + describe('When on the Trusted Apps list', function () { this.tags('ciGroup7'); - describe('when there is data', () => { - before(async () => { - await pageObjects.trustedApps.navigateToTrustedAppsList(); - }); + before(async () => { + await pageObjects.trustedApps.navigateToTrustedAppsList(); + }); + + it('should show page title', async () => { + expect(await testSubjects.getVisibleText('header-page-title')).to.equal( + 'Trusted Applications BETA' + ); + }); + + it('should be able to add a new trusted app and remove it', async () => { + const SHA256 = 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476'; + + // Add it + await testSubjects.click('trustedAppsListAddButton'); + await testSubjects.setValue( + 'addTrustedAppFlyout-createForm-nameTextField', + 'Windows Defender' + ); + await testSubjects.setValue( + 'addTrustedAppFlyout-createForm-conditionsBuilder-group1-entry0-value', + SHA256 + ); + await testSubjects.click('addTrustedAppFlyout-createButton'); + expect(await testSubjects.getVisibleText('conditionValue')).to.equal(SHA256.toLowerCase()); + await pageObjects.common.closeToast(); - it('finds page title', async () => { - expect(await testSubjects.getVisibleText('header-page-title')).to.equal( - 'Trusted applications BETA' - ); - }); + // Remove it + await testSubjects.click('trustedAppDeleteButton'); + await testSubjects.click('trustedAppDeletionConfirm'); + await testSubjects.waitForDeleted('trustedAppDeletionConfirm'); + expect(await testSubjects.getVisibleText('trustedAppsListViewCountLabel')).to.equal( + '0 trusted applications' + ); }); }); }; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index b57486ee55ca4..0878c09cff500 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -5,10 +5,7 @@ */ import expect from '@kbn/expect'; import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { - ResolverPaginatedEvents, - SafeResolverRelatedEvents, -} from '../../../../plugins/security_solution/common/endpoint/types'; +import { ResolverPaginatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -20,7 +17,6 @@ import { compareArrays } from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); - const esArchiver = getService('esArchiver'); const relatedEventsToGen = [ { category: RelatedEventCategory.Driver, count: 2 }, @@ -44,308 +40,136 @@ export default function ({ getService }: FtrProviderContext) { ancestryArraySize: 2, }; - describe('event routes', () => { - describe('related events route', () => { - before(async () => { - await esArchiver.load('endpoint/resolver/api_feature'); - resolverTrees = await resolver.createTrees(treeOptions); - // we only requested a single alert so there's only 1 tree - tree = resolverTrees.trees[0]; - }); - after(async () => { - await resolver.deleteData(resolverTrees); - await esArchiver.unload('endpoint/resolver/api_feature'); - }); - - describe('legacy events', () => { - const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; - const entityID = '94042'; - const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; - - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(1); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('returns no values when there is no more data', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - // after is set to the document id of the last event so there shouldn't be any more after it - .post( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events).be.empty(); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.entityID).to.eql(entityID); - expect(body.events).to.be.empty(); - }); - - it('should error on invalid pagination values', async () => { - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=0`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) - .set('kbn-xsrf', 'xxx') - .expect(400); - }); - }); - - describe('endpoint events', () => { - it('should not find any events', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/5555/events`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.events).to.be.empty(); - }); - - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - - it('should allow for the events to be filtered', async () => { - const filter = `event.category:"${RelatedEventCategory.Driver}"`; - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter, - }) - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.eql(null); - for (const event of body.events) { - expect(event.event?.category).to.be(RelatedEventCategory.Driver); - } - }); - - it('should return paginated results for the root node', async () => { - let { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).not.to.eql(null); - - ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .set('kbn-xsrf', 'xxx') - .expect(200)); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.not.eql(null); - - ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) - .set('kbn-xsrf', 'xxx') - .expect(200)); - expect(body.events).to.be.empty(); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); - - it('should sort the events in descending order', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(4); - // these events are created in the order they are defined in the array so the newest one is - // the last element in the array so let's reverse it - const relatedEvents = tree.origin.relatedEvents.reverse(); - for (let i = 0; i < body.events.length; i++) { - expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); - expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); - } - }); - }); + describe('event route', () => { + let entityIDFilter: string | undefined; + before(async () => { + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + entityIDFilter = `process.entity_id:"${tree.origin.id}" and not event.category:"process"`; + }); + after(async () => { + await resolver.deleteData(resolverTrees); }); - describe('kql events route', () => { - let entityIDFilter: string | undefined; - before(async () => { - resolverTrees = await resolver.createTrees(treeOptions); - // we only requested a single alert so there's only 1 tree - tree = resolverTrees.trees[0]; - entityIDFilter = `process.entity_id:"${tree.origin.id}" and not event.category:"process"`; - }); - after(async () => { - await resolver.deleteData(resolverTrees); - }); - - it('should filter events by event.id', async () => { - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: `event.id:"${tree.origin.relatedEvents[0]?.event?.id}"`, - }) - .expect(200); - expect(body.events.length).to.eql(1); - expect(tree.origin.relatedEvents[0]?.event?.id).to.eql(body.events[0].event?.id); - expect(body.nextEvent).to.eql(null); - }); - - it('should not find any events when given an invalid entity id', async () => { - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: 'process.entity_id:"5555"', - }) - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.events).to.be.empty(); - }); - - it('should return related events for the root node', async () => { - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); + it('should filter events by event.id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: `event.id:"${tree.origin.relatedEvents[0]?.event?.id}"`, + }) + .expect(200); + expect(body.events.length).to.eql(1); + expect(tree.origin.relatedEvents[0]?.event?.id).to.eql(body.events[0].event?.id); + expect(body.nextEvent).to.eql(null); + }); - it('should allow for the events to be filtered', async () => { - const filter = `event.category:"${RelatedEventCategory.Driver}" and ${entityIDFilter}`; - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter, - }) - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.eql(null); - for (const event of body.events) { - expect(event.event?.category).to.be(RelatedEventCategory.Driver); - } - }); + it('should not find any events when given an invalid entity id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: 'process.entity_id:"5555"', + }) + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.events).to.be.empty(); + }); - it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events?limit=2`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).not.to.eql(null); + it('should return related events for the root node', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); - ({ body } = await supertest - .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200)); - expect(body.events.length).to.eql(2); - compareArrays(tree.origin.relatedEvents, body.events); - expect(body.nextEvent).to.not.eql(null); + it('should allow for the events to be filtered', async () => { + const filter = `event.category:"${RelatedEventCategory.Driver}" and ${entityIDFilter}`; + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.eql(null); + for (const event of body.events) { + expect(event.event?.category).to.be(RelatedEventCategory.Driver); + } + }); - ({ body } = await supertest - .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200)); - expect(body.events).to.be.empty(); - expect(body.nextEvent).to.eql(null); - }); + it('should return paginated results for the root node', async () => { + let { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?limit=2`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200)); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.not.eql(null); + + ({ body } = await supertest + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200)); + expect(body.events).to.be.empty(); + expect(body.nextEvent).to.eql(null); + }); - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events?afterEvent=blah`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200); - expect(body.events.length).to.eql(4); - compareArrays(tree.origin.relatedEvents, body.events, true); - expect(body.nextEvent).to.eql(null); - }); + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?afterEvent=blah`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); - it('should sort the events in descending order', async () => { - const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events`) - .set('kbn-xsrf', 'xxx') - .send({ - filter: entityIDFilter, - }) - .expect(200); - expect(body.events.length).to.eql(4); - // these events are created in the order they are defined in the array so the newest one is - // the last element in the array so let's reverse it - const relatedEvents = tree.origin.relatedEvents.reverse(); - for (let i = 0; i < body.events.length; i++) { - expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); - expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); - } - }); + it('should sort the events in descending order', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) + .expect(200); + expect(body.events.length).to.eql(4); + // these events are created in the order they are defined in the array so the newest one is + // the last element in the array so let's reverse it + const relatedEvents = tree.origin.relatedEvents.reverse(); + for (let i = 0; i < body.events.length; i++) { + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); + expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); + } }); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 837af6a940f5c..7a95bf7bab883 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -340,10 +340,8 @@ export default function ({ getService }: FtrProviderContext) { .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) .expect(200); expect(body.ancestry.nextAncestor).to.equal(null); - expect(body.relatedEvents.nextEvent).to.equal(null); expect(body.children.nextChild).to.equal(null); expect(body.children.childNodes.length).to.equal(0); - expect(body.relatedEvents.events.length).to.equal(0); expect(body.lifecycle.length).to.equal(2); }); }); @@ -365,9 +363,6 @@ export default function ({ getService }: FtrProviderContext) { verifyAncestry(body.ancestry.ancestors, tree, true); verifyLifecycleStats(body.ancestry.ancestors, relatedEventsToGen, relatedAlerts); - expect(body.relatedEvents.nextEvent).to.equal(null); - compareArrays(tree.origin.relatedEvents, body.relatedEvents.events, true); - expect(body.relatedAlerts.nextAlert).to.equal(null); compareArrays(tree.origin.relatedAlerts, body.relatedAlerts.alerts, true); diff --git a/yarn.lock b/yarn.lock index eb12df7b50d72..200896a9ce1a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8114,15 +8114,6 @@ caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.300010 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001114.tgz#2e88119afb332ead5eaa330e332e951b1c4bfea9" integrity sha512-ml/zTsfNBM+T1+mjglWRPgVsu2L76GAaADKX5f4t0pbhttEp0WMawJsHDYlFkVZkoA+89uvBRrVrEE4oqenzXQ== -canvas@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.6.1.tgz#0d087dd4d60f5a5a9efa202757270abea8bef89e" - integrity sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA== - dependencies: - nan "^2.14.0" - node-pre-gyp "^0.11.0" - simple-get "^3.0.3" - capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -10607,11 +10598,6 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - detect-newline@2.X: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -15382,7 +15368,7 @@ icalendar@0.7.1: resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" integrity sha1-0NNIZ5X48cXPT4yvrAgbS056Mq4= -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -15440,13 +15426,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - ignore@^3.1.2, ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -18889,10 +18868,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.10.0.tgz#c33e74d1f328e820e245ff8ed7b5dbbbc4be204f" - integrity sha512-SrJXcR9s5yEsPuW2kKKumA1KqYW9RrL8j7ZcIh6glRQ/x3lwNMfwz/UEJAJcVNgeX+fiwzuBoDIdeGB/vSkZLQ== +mapbox-gl@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" + integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" @@ -18914,7 +18893,7 @@ mapbox-gl@^1.10.0: potpack "^1.0.1" quickselect "^2.0.0" rw "^1.3.3" - supercluster "^7.0.0" + supercluster "^7.1.0" tinyqueue "^2.0.3" vt-pbf "^3.1.1" @@ -19912,15 +19891,6 @@ nearley@^2.7.10: randexp "0.4.6" semver "^5.4.1" -needle@^2.2.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.0.tgz#e6fc4b3cc6c25caed7554bd613a5cf0bac8c31c0" - integrity sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -20163,22 +20133,6 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - node-preload@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" @@ -20269,7 +20223,7 @@ nopt@^2.2.0: dependencies: abbrev "1" -nopt@^4.0.1, nopt@^4.0.3: +nopt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== @@ -20347,13 +20301,6 @@ now-and-later@^2.0.0: dependencies: once "^1.3.2" -npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== - dependencies: - npm-normalize-package-bin "^1.0.1" - npm-conf@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" @@ -20370,20 +20317,11 @@ npm-keyword@^5.0.0: got "^7.1.0" registry-url "^3.0.3" -npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: +npm-normalize-package-bin@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - npm-run-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" @@ -20421,7 +20359,7 @@ npmconf@^2.1.3: semver "2 || 3 || 4" uid-number "0.0.5" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2: +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -24608,7 +24546,7 @@ sass-resources-loader@^2.0.1: glob "^7.1.1" loader-utils "^1.0.4" -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -25048,20 +24986,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -simple-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" - integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= - -simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - simple-git@1.116.0: version "1.116.0" resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.116.0.tgz#ea6e533466f1e0152186e306e004d4eefa6e3e00" @@ -26085,10 +26009,10 @@ superagent@3.8.2: qs "^6.5.1" readable-stream "^2.0.5" -supercluster@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.0.0.tgz#75d474fafb0a055db552ed7bd7bbda583f6ab321" - integrity sha512-8VuHI8ynylYQj7Qf6PBMWy1PdgsnBiIxujOgc9Z83QvJ8ualIYWNx2iMKyKeC4DZI5ntD9tz/CIwwZvIelixsA== +supercluster@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.0.tgz#f0a457426ec0ab95d69c5f03b51e049774b94479" + integrity sha512-LDasImUAFMhTqhK+cUXfy9C2KTUqJ3gucLjmNLNFmKWOnDUBxLFLH9oKuXOTCLveecmxh8fbk8kgh6Q0gsfe2w== dependencies: kdbush "^3.0.0" @@ -26370,7 +26294,7 @@ tar-stream@^2.0.0, tar-stream@^2.1.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@4.4.13, tar@^4: +tar@4.4.13: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -27082,11 +27006,16 @@ tslib@^1, tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.0.0, tslib@~2.0.1: +tslib@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== +tslib@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -28166,10 +28095,10 @@ vega-label@~1.0.0: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^4.16.8: - version "4.16.8" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.16.8.tgz#23a91f9b87a97c7ffc6d754d0ec8f6a3b04d6976" - integrity sha512-WB9OOHbFyIaLvx5k9m8XGEaB2p0sTC9Srtsm9ETQ6EoOksdLQtVesxCalgT+cGaUVtHAiqBNmLh/nQGxZXml7w== +vega-lite@^4.17.0: + version "4.17.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.17.0.tgz#01ad4535e92f28c3852c1071711de272ddfb4631" + integrity sha512-MO2XsaVZqx6iWWmVA5vwYFamvhRUsKfVp7n0pNlkZ2/21cuxelSl92EePZ2YGmzL6z4/3K7r/45zaG8p+qNHeg== dependencies: "@types/clone" "~2.1.0" "@types/fast-json-stable-stringify" "^2.0.0" @@ -28178,10 +28107,10 @@ vega-lite@^4.16.8: fast-deep-equal "~3.1.3" fast-json-stable-stringify "~2.1.0" json-stringify-pretty-compact "~2.0.0" - tslib "~2.0.1" + tslib "~2.0.3" vega-event-selector "~2.0.6" vega-expression "~3.0.0" - vega-util "~1.15.3" + vega-util "~1.16.0" yargs "~16.0.3" vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: @@ -28319,11 +28248,6 @@ vega-util@^1.15.2, vega-util@^1.16.0, vega-util@~1.16.0: resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.16.0.tgz#77405d8df0a94944d106bdc36015f0d43aa2caa3" integrity sha512-6mmz6mI+oU4zDMeKjgvE2Fjz0Oh6zo6WGATcvCfxH2gXBzhBHmy5d25uW5Zjnkc6QBXSWPLV9Xa6SiqMsrsKog== -vega-util@~1.15.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.15.3.tgz#b42b4fb11f32fbb57fb5cd116d4d3e1827d177aa" - integrity sha512-NCbfCPMVgdP4geLrFtCDN9PTEXrgZgJBBLvpyos7HGv2xSe9bGjDCysv6qcueHrc1myEeCQzrHDFaShny6wXDg== - vega-view-transforms@~4.5.8: version "4.5.8" resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz#c8dc42c3c7d4aa725d40b8775180c9f23bc98f4e"