diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5a9e8bc585119..525da9d832b53 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,8 +381,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib **/*.scss @elastic/kibana-design #CC# /packages/kbn-ui-framework/ @elastic/kibana-design -# Core design +# Core UI design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers +/src/plugins/embeddable/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers diff --git a/.telemetryrc.json b/.telemetryrc.json index 3d1b0df1d8f93..0f1530c6225d6 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -7,5 +7,10 @@ "src/plugins/testbed/", "src/plugins/kibana_utils/" ] + }, + { + "output": "src/plugins/telemetry/schema/legacy_plugins.json", + "root": "src/legacy/server/", + "exclude": [] } ] diff --git a/docs/apm/images/advanced-discover.png b/docs/apm/images/advanced-discover.png index 56ba58b2c1d41..5291526783a6b 100644 Binary files a/docs/apm/images/advanced-discover.png and b/docs/apm/images/advanced-discover.png differ diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 6c52c021fc0fc..7084777cbb6f9 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -157,7 +157,7 @@ the values in `http.request.cookies` are not indexed and thus not searchable. *Ensure an index pattern exists* As a first step, you should ensure the correct index pattern exists. -In Kibana, navigate to *Management > Kibana > Index Patterns*. +Open the main menu, then click *Stack Management > Index Patterns*. In the pattern list, you should see an apm index pattern; The default is `apm-*`. If you don't, the index pattern doesn't exist. See <> for information on how to fix this problem. diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index 312391541a777..6456ba02bb8a8 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -14,7 +14,7 @@ For this tutorial, you'll need to add the <>, and work with data in other contexts. -To get started, open the menu, go to *Dev Tools*, then click *Painless Lab*. +To get started, open the main menu, click *Dev Tools*, then click *Painless Lab*. image::dev-tools/painlesslab/images/painless-lab.png[Painless Lab] diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index eaa7fea6c7f8d..7cd54db5562b7 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -2,7 +2,7 @@ [[profiler-getting-started]] === Getting Started -The {searchprofiler} is automatically enabled in {kib}. From the menu, go to *Dev Tools*, then click *Search Profiler* +The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *Search Profiler* to get started. {searchprofiler} displays the names of the indices searched, the shards in each index, diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md index f8966572afbb6..051414eac7585 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror._constructor_.md @@ -9,13 +9,13 @@ Constructs a new instance of the `PainlessError` class Signature: ```typescript -constructor(err: EsError, request: IKibanaSearchRequest); +constructor(err: IEsError, request: IKibanaSearchRequest); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| err | EsError | | +| err | IEsError | | | request | IKibanaSearchRequest | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md index 306211cd60259..6ab32f3fb1dfa 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.painlesserror.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class PainlessError extends KbnError +export declare class PainlessError extends EsError ``` ## Constructors diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md index d36ebd0745e8d..214c795fda9d1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md @@ -9,12 +9,13 @@ Constructs a new instance of the `IndexPatternsFetcher` class Signature: ```typescript -constructor(callDataCluster: LegacyAPICaller); +constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| callDataCluster | LegacyAPICaller | | +| elasticsearchClient | ElasticsearchClient | | +| allowNoIndices | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index 52382372d6d96..addd29916d81d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -13,7 +13,7 @@ getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { - allowNoIndices: boolean; + allow_no_indices: boolean; }; }): Promise; ``` @@ -22,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allowNoIndices: boolean;
};
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index f71a702f3381d..3ba3c862bf16a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -14,7 +14,7 @@ export declare class IndexPatternsFetcher | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(callDataCluster)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher class | +| [(constructor)(elasticsearchClient, allowNoIndices)](./kibana-plugin-plugins-data-server.indexpatternsfetcher._constructor_.md) | | Constructs a new instance of the IndexPatternsFetcher class | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution._constructor_.md index 1d0c9f99169db..14a0f8818e903 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution._constructor_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution._constructor_.md @@ -9,12 +9,12 @@ Constructs a new instance of the `Execution` class Signature: ```typescript -constructor(params: ExecutionParams); +constructor(execution: ExecutionParams); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| params | ExecutionParams<ExtraContext> | | +| execution | ExecutionParams | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.context.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.context.md index 732fe94d65617..e884db46563b5 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.context.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.context.md @@ -9,5 +9,5 @@ Execution context - object that allows to do side-effects. Context is passed to Signature: ```typescript -readonly context: ExecutionContext & ExtraContext; +readonly context: ExecutionContext; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.contract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.contract.md index fa03297ea22a7..383e9ee3e81b8 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.contract.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.contract.md @@ -9,5 +9,5 @@ Contract is a public representation of `Execution` instances. Contract we can re Signature: ```typescript -readonly contract: ExecutionContract; +readonly contract: ExecutionContract; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.params.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.execution.md similarity index 67% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.params.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.execution.md index cd90bf6adab47..eebb5cf5440d5 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.params.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.execution.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Execution](./kibana-plugin-plugins-expressions-public.execution.md) > [params](./kibana-plugin-plugins-expressions-public.execution.params.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Execution](./kibana-plugin-plugins-expressions-public.execution.md) > [execution](./kibana-plugin-plugins-expressions-public.execution.execution.md) -## Execution.params property +## Execution.execution property Signature: ```typescript -readonly params: ExecutionParams; +readonly execution: ExecutionParams; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md index 31f38b7069812..24dee04861b4e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.interpret.md @@ -7,7 +7,7 @@ Signature: ```typescript -interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): Promise; +interpret(ast: ExpressionAstNode, input: T): Promise; ``` ## Parameters @@ -16,7 +16,6 @@ interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): | --- | --- | --- | | ast | ExpressionAstNode | | | input | T | | -| options | ExpressionExecOptions | | Returns: diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md index 4d227e6ab85b8..56b14e005adfb 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md @@ -7,25 +7,25 @@ Signature: ```typescript -export declare class Execution = Record, Input = unknown, Output = unknown, InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters> +export declare class Execution ``` ## Constructors | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(params)](./kibana-plugin-plugins-expressions-public.execution._constructor_.md) | | Constructs a new instance of the Execution class | +| [(constructor)(execution)](./kibana-plugin-plugins-expressions-public.execution._constructor_.md) | | Constructs a new instance of the Execution class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [context](./kibana-plugin-plugins-expressions-public.execution.context.md) | | ExecutionContext<Input, InspectorAdapters> & ExtraContext | Execution context - object that allows to do side-effects. Context is passed to every function. | -| [contract](./kibana-plugin-plugins-expressions-public.execution.contract.md) | | ExecutionContract<ExtraContext, Input, Output, InspectorAdapters> | Contract is a public representation of Execution instances. Contract we can return to other plugins for their consumption. | +| [context](./kibana-plugin-plugins-expressions-public.execution.context.md) | | ExecutionContext<InspectorAdapters> | Execution context - object that allows to do side-effects. Context is passed to every function. | +| [contract](./kibana-plugin-plugins-expressions-public.execution.contract.md) | | ExecutionContract<Input, Output, InspectorAdapters> | Contract is a public representation of Execution instances. Contract we can return to other plugins for their consumption. | +| [execution](./kibana-plugin-plugins-expressions-public.execution.execution.md) | | ExecutionParams | | | [expression](./kibana-plugin-plugins-expressions-public.execution.expression.md) | | string | | | [input](./kibana-plugin-plugins-expressions-public.execution.input.md) | | Input | Initial input of the execution.N.B. It is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.execution.inspectoradapters.md) | | InspectorAdapters | | -| [params](./kibana-plugin-plugins-expressions-public.execution.params.md) | | ExecutionParams<ExtraContext> | | | [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | Promise<Output | ExpressionValueError> | | | [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | @@ -35,7 +35,7 @@ export declare class Execution = Re | --- | --- | --- | | [cancel()](./kibana-plugin-plugins-expressions-public.execution.cancel.md) | | Stop execution of expression. | | [cast(value, toTypeNames)](./kibana-plugin-plugins-expressions-public.execution.cast.md) | | | -| [interpret(ast, input, options)](./kibana-plugin-plugins-expressions-public.execution.interpret.md) | | | +| [interpret(ast, input)](./kibana-plugin-plugins-expressions-public.execution.interpret.md) | | | | [invokeChain(chainArr, input)](./kibana-plugin-plugins-expressions-public.execution.invokechain.md) | | | | [invokeFunction(fn, input, args)](./kibana-plugin-plugins-expressions-public.execution.invokefunction.md) | | | | [resolveArgs(fnDef, input, argAsts)](./kibana-plugin-plugins-expressions-public.execution.resolveargs.md) | | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getinitialinput.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md similarity index 55% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getinitialinput.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md index 460b1622c6fbd..471e18ee6a7eb 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getinitialinput.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getInitialInput](./kibana-plugin-plugins-expressions-public.executioncontext.getinitialinput.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) -## ExecutionContext.getInitialInput property +## ExecutionContext.getSearchContext property -Get initial input with which execution started. +Get search context of the expression. Signature: ```typescript -getInitialInput: () => Input; +getSearchContext: () => ExecutionContextSearch; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.search.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md similarity index 63% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.search.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md index 05501a475cbd4..107ae16dc8901 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.search.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [search](./kibana-plugin-plugins-expressions-public.executioncontext.search.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) -## ExecutionContext.search property +## ExecutionContext.getSearchSessionId property Search context in which expression should operate. Signature: ```typescript -search?: ExecutionContextSearch; +getSearchSessionId: () => string | undefined; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 786e94455c600..2a1a78b8fcb1a 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties @@ -17,10 +17,10 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getInitialInput](./kibana-plugin-plugins-expressions-public.executioncontext.getinitialinput.md) | () => Input | Get initial input with which execution started. | | [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) | <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>> | Allows to fetch saved objects from ElasticSearch. In browser getSavedObject function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | +| [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | +| [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | -| [search](./kibana-plugin-plugins-expressions-public.executioncontext.search.md) | ExecutionContextSearch | Search context in which expression should operate. | | [types](./kibana-plugin-plugins-expressions-public.executioncontext.types.md) | Record<string, ExpressionType> | A map of available expression types. | | [variables](./kibana-plugin-plugins-expressions-public.executioncontext.variables.md) | Record<string, unknown> | Context variables that can be consumed using var and var_set functions. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract._constructor_.md index 89a99ef2f8ef8..ee8b113881a05 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract._constructor_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract._constructor_.md @@ -9,12 +9,12 @@ Constructs a new instance of the `ExecutionContract` class Signature: ```typescript -constructor(execution: Execution); +constructor(execution: Execution); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| execution | Execution<ExtraContext, Input, Output, InspectorAdapters> | | +| execution | Execution<Input, Output, InspectorAdapters> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.execution.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.execution.md index f7911250488f2..aa058c71c12df 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.execution.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.execution.md @@ -7,5 +7,5 @@ Signature: ```typescript -protected readonly execution: Execution; +protected readonly execution: Execution; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.md index d05620eace208..f2c050bbfe0ba 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.md @@ -9,7 +9,7 @@ Signature: ```typescript -export declare class ExecutionContract = Record, Input = unknown, Output = unknown, InspectorAdapters = unknown> +export declare class ExecutionContract ``` ## Constructors @@ -23,7 +23,7 @@ export declare class ExecutionContract() => void | Cancel the execution of the expression. This will set abort signal (available in execution context) to aborted state, letting expression functions to stop their execution. | -| [execution](./kibana-plugin-plugins-expressions-public.executioncontract.execution.md) | | Execution<ExtraContext, Input, Output, InspectorAdapters> | | +| [execution](./kibana-plugin-plugins-expressions-public.executioncontract.execution.md) | | Execution<Input, Output, InspectorAdapters> | | | [getAst](./kibana-plugin-plugins-expressions-public.executioncontract.getast.md) | | () => ExpressionAstExpression | Get AST used to execute the expression. | | [getData](./kibana-plugin-plugins-expressions-public.executioncontract.getdata.md) | | () => Promise<Output | ExpressionValueError> | Returns the final output of expression, if any error happens still wraps that error into ExpressionValueError type and returns that. This function never throws. | | [getExpression](./kibana-plugin-plugins-expressions-public.executioncontract.getexpression.md) | | () => string | Get string representation of the expression. Returns the original string if execution was started from a string. If execution was started from an AST this method returns a string generated from AST. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.debug.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.debug.md deleted file mode 100644 index 61ec72465f55e..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-public.executionparams.md) > [debug](./kibana-plugin-plugins-expressions-public.executionparams.debug.md) - -## ExecutionParams.debug property - -Whether to execute expression in \*debug mode\*. In \*debug mode\* inputs and outputs as well as all resolved arguments and time it took to execute each function are saved and are available for introspection. - -Signature: - -```typescript -debug?: boolean; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.md index e39dc231fbf96..6e5d70c61ead6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface ExecutionParams = Record> +export interface ExecutionParams ``` ## Properties @@ -15,8 +15,7 @@ export interface ExecutionParams = | Property | Type | Description | | --- | --- | --- | | [ast](./kibana-plugin-plugins-expressions-public.executionparams.ast.md) | ExpressionAstExpression | | -| [context](./kibana-plugin-plugins-expressions-public.executionparams.context.md) | ExtraContext | | -| [debug](./kibana-plugin-plugins-expressions-public.executionparams.debug.md) | boolean | Whether to execute expression in \*debug mode\*. In \*debug mode\* inputs and outputs as well as all resolved arguments and time it took to execute each function are saved and are available for introspection. | | [executor](./kibana-plugin-plugins-expressions-public.executionparams.executor.md) | Executor<any> | | | [expression](./kibana-plugin-plugins-expressions-public.executionparams.expression.md) | string | | +| [params](./kibana-plugin-plugins-expressions-public.executionparams.params.md) | ExpressionExecutionParams | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.context.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.params.md similarity index 65% rename from docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.context.md rename to docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.params.md index b6013162ef2ae..0dbe87bfda79e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.context.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executionparams.params.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-public.executionparams.md) > [context](./kibana-plugin-plugins-expressions-public.executionparams.context.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-public.executionparams.md) > [params](./kibana-plugin-plugins-expressions-public.executionparams.params.md) -## ExecutionParams.context property +## ExecutionParams.params property Signature: ```typescript -context?: ExtraContext; +params: ExpressionExecutionParams; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.createexecution.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.createexecution.md index e6765064d4a27..2832ba92262f2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.createexecution.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.createexecution.md @@ -7,7 +7,7 @@ Signature: ```typescript -createExecution = Record, Input = unknown, Output = unknown>(ast: string | ExpressionAstExpression, context?: ExtraContext, { debug }?: ExpressionExecOptions): Execution; +createExecution(ast: string | ExpressionAstExpression, params?: ExpressionExecutionParams): Execution; ``` ## Parameters @@ -15,10 +15,9 @@ createExecution = Recordstring | ExpressionAstExpression | | -| context | ExtraContext | | -| { debug } | ExpressionExecOptions | | +| params | ExpressionExecutionParams | | Returns: -`Execution` +`Execution` 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 013624f30b45a..aefd04112dc1c 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 @@ -29,7 +29,7 @@ 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) | | | @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; ``` ## Parameters @@ -18,8 +18,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | -| context | ExtraContext | | -| options | ExpressionExecOptions | | +| params | ExpressionExecutionParams | | Returns: diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md index 18b856b946da4..043d3472228a2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md @@ -9,5 +9,5 @@ Starts expression execution and immediately returns `ExecutionContract` instance Signature: ```typescript -execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; +execute: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => ExecutionContract; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md index def572abead22..6b678fc4fbc26 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md @@ -16,12 +16,12 @@ export interface ExpressionsServiceStart | Property | Type | Description | | --- | --- | --- | -| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract<ExtraContext, Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | +| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => ExecutionContract<Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | | [fork](./kibana-plugin-plugins-expressions-public.expressionsservicestart.fork.md) | () => ExpressionsService | Create a new instance of ExpressionsService. The new instance inherits all state of the original ExpressionsService, including all expression types, expression functions and context. Also, all new types and functions registered in the original services AFTER the forking event will be available in the forked instance. However, all new types and functions registered in the forked instances will NOT be available to the original service. | | [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getfunction.md) | (name: string) => ReturnType<Executor['getFunction']> | Get a registered ExpressionFunction by its name, which was registered using the registerFunction method. The returned ExpressionFunction instance is an internal representation of the function in Expressions service - do not mutate that object. | | [getRenderer](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getrenderer.md) | (name: string) => ReturnType<ExpressionRendererRegistry['get']> | Get a registered ExpressionRenderer by its name, which was registered using the registerRenderer method. The returned ExpressionRenderer instance is an internal representation of the renderer in Expressions service - do not mutate that object. | | [getType](./kibana-plugin-plugins-expressions-public.expressionsservicestart.gettype.md) | (name: string) => ReturnType<Executor['getType']> | Get a registered ExpressionType by its name, which was registered using the registerType method. The returned ExpressionType instance is an internal representation of the type in Expressions service - do not mutate that object. | -| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. +| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. ```ts expressions.run('sleep 100 | clog', 123); diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md index d717af51a00fa..9efca0011174c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md @@ -24,5 +24,5 @@ expressions.run('...', null, { elasticsearchClient }); Signature: ```typescript -run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; +run: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index d6e02350bae3f..e2ad6215e25d0 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -22,6 +22,7 @@ export interface IExpressionLoaderParams | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | ExecutionContextSearch | | +| [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md new file mode 100644 index 0000000000000..bb021b003f0d3 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) + +## IExpressionLoaderParams.searchSessionId property + +Signature: + +```typescript +searchSessionId?: string; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution._constructor_.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution._constructor_.md index 75f4cc4c2a017..f24aae8603b7d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution._constructor_.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution._constructor_.md @@ -9,12 +9,12 @@ Constructs a new instance of the `Execution` class Signature: ```typescript -constructor(params: ExecutionParams); +constructor(execution: ExecutionParams); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| params | ExecutionParams<ExtraContext> | | +| execution | ExecutionParams | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.context.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.context.md index d1969fb0859b7..65c7bdca0fe5d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.context.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.context.md @@ -9,5 +9,5 @@ Execution context - object that allows to do side-effects. Context is passed to Signature: ```typescript -readonly context: ExecutionContext & ExtraContext; +readonly context: ExecutionContext; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.contract.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.contract.md index 149b5a7ced9cb..2fc6a38997f77 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.contract.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.contract.md @@ -9,5 +9,5 @@ Contract is a public representation of `Execution` instances. Contract we can re Signature: ```typescript -readonly contract: ExecutionContract; +readonly contract: ExecutionContract; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.params.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.execution.md similarity index 67% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.params.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.execution.md index 498f9bbfccfa4..acaccdeab7321 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.params.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.execution.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Execution](./kibana-plugin-plugins-expressions-server.execution.md) > [params](./kibana-plugin-plugins-expressions-server.execution.params.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Execution](./kibana-plugin-plugins-expressions-server.execution.md) > [execution](./kibana-plugin-plugins-expressions-server.execution.execution.md) -## Execution.params property +## Execution.execution property Signature: ```typescript -readonly params: ExecutionParams; +readonly execution: ExecutionParams; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md index cf59e796e6120..e425bdc70e349 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.interpret.md @@ -7,7 +7,7 @@ Signature: ```typescript -interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): Promise; +interpret(ast: ExpressionAstNode, input: T): Promise; ``` ## Parameters @@ -16,7 +16,6 @@ interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): | --- | --- | --- | | ast | ExpressionAstNode | | | input | T | | -| options | ExpressionExecOptions | | Returns: diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md index fc663dd115580..c94ae9bcfe946 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.md @@ -7,25 +7,25 @@ Signature: ```typescript -export declare class Execution = Record, Input = unknown, Output = unknown, InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters> +export declare class Execution ``` ## Constructors | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(params)](./kibana-plugin-plugins-expressions-server.execution._constructor_.md) | | Constructs a new instance of the Execution class | +| [(constructor)(execution)](./kibana-plugin-plugins-expressions-server.execution._constructor_.md) | | Constructs a new instance of the Execution class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [context](./kibana-plugin-plugins-expressions-server.execution.context.md) | | ExecutionContext<Input, InspectorAdapters> & ExtraContext | Execution context - object that allows to do side-effects. Context is passed to every function. | -| [contract](./kibana-plugin-plugins-expressions-server.execution.contract.md) | | ExecutionContract<ExtraContext, Input, Output, InspectorAdapters> | Contract is a public representation of Execution instances. Contract we can return to other plugins for their consumption. | +| [context](./kibana-plugin-plugins-expressions-server.execution.context.md) | | ExecutionContext<InspectorAdapters> | Execution context - object that allows to do side-effects. Context is passed to every function. | +| [contract](./kibana-plugin-plugins-expressions-server.execution.contract.md) | | ExecutionContract<Input, Output, InspectorAdapters> | Contract is a public representation of Execution instances. Contract we can return to other plugins for their consumption. | +| [execution](./kibana-plugin-plugins-expressions-server.execution.execution.md) | | ExecutionParams | | | [expression](./kibana-plugin-plugins-expressions-server.execution.expression.md) | | string | | | [input](./kibana-plugin-plugins-expressions-server.execution.input.md) | | Input | Initial input of the execution.N.B. It is initialized to null rather than undefined for legacy reasons, because in legacy interpreter it was set to null by default. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-server.execution.inspectoradapters.md) | | InspectorAdapters | | -| [params](./kibana-plugin-plugins-expressions-server.execution.params.md) | | ExecutionParams<ExtraContext> | | | [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | Promise<Output | ExpressionValueError> | | | [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | @@ -35,7 +35,7 @@ export declare class Execution = Re | --- | --- | --- | | [cancel()](./kibana-plugin-plugins-expressions-server.execution.cancel.md) | | Stop execution of expression. | | [cast(value, toTypeNames)](./kibana-plugin-plugins-expressions-server.execution.cast.md) | | | -| [interpret(ast, input, options)](./kibana-plugin-plugins-expressions-server.execution.interpret.md) | | | +| [interpret(ast, input)](./kibana-plugin-plugins-expressions-server.execution.interpret.md) | | | | [invokeChain(chainArr, input)](./kibana-plugin-plugins-expressions-server.execution.invokechain.md) | | | | [invokeFunction(fn, input, args)](./kibana-plugin-plugins-expressions-server.execution.invokefunction.md) | | | | [resolveArgs(fnDef, input, argAsts)](./kibana-plugin-plugins-expressions-server.execution.resolveargs.md) | | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getinitialinput.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md similarity index 55% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getinitialinput.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md index b5f9b91e1c7b7..783841fbafa92 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getinitialinput.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getInitialInput](./kibana-plugin-plugins-expressions-server.executioncontext.getinitialinput.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) -## ExecutionContext.getInitialInput property +## ExecutionContext.getSearchContext property -Get initial input with which execution started. +Get search context of the expression. Signature: ```typescript -getInitialInput: () => Input; +getSearchContext: () => ExecutionContextSearch; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.search.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md similarity index 63% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.search.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md index 641e50696f6e0..a69f0ed32bd51 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.search.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [search](./kibana-plugin-plugins-expressions-server.executioncontext.search.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) -## ExecutionContext.search property +## ExecutionContext.getSearchSessionId property Search context in which expression should operate. Signature: ```typescript -search?: ExecutionContextSearch; +getSearchSessionId: () => string | undefined; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 0128ba934da73..047879fd99255 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties @@ -17,10 +17,10 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getInitialInput](./kibana-plugin-plugins-expressions-server.executioncontext.getinitialinput.md) | () => Input | Get initial input with which execution started. | | [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) | <T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>> | Allows to fetch saved objects from ElasticSearch. In browser getSavedObject function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. | +| [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | +| [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-server.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | -| [search](./kibana-plugin-plugins-expressions-server.executioncontext.search.md) | ExecutionContextSearch | Search context in which expression should operate. | | [types](./kibana-plugin-plugins-expressions-server.executioncontext.types.md) | Record<string, ExpressionType> | A map of available expression types. | | [variables](./kibana-plugin-plugins-expressions-server.executioncontext.variables.md) | Record<string, unknown> | Context variables that can be consumed using var and var_set functions. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.debug.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.debug.md deleted file mode 100644 index b3631e0aeebe6..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.debug.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-server.executionparams.md) > [debug](./kibana-plugin-plugins-expressions-server.executionparams.debug.md) - -## ExecutionParams.debug property - -Whether to execute expression in \*debug mode\*. In \*debug mode\* inputs and outputs as well as all resolved arguments and time it took to execute each function are saved and are available for introspection. - -Signature: - -```typescript -debug?: boolean; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.md index a7594bff48c1a..6a901c91ffff1 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface ExecutionParams = Record> +export interface ExecutionParams ``` ## Properties @@ -15,8 +15,7 @@ export interface ExecutionParams = | Property | Type | Description | | --- | --- | --- | | [ast](./kibana-plugin-plugins-expressions-server.executionparams.ast.md) | ExpressionAstExpression | | -| [context](./kibana-plugin-plugins-expressions-server.executionparams.context.md) | ExtraContext | | -| [debug](./kibana-plugin-plugins-expressions-server.executionparams.debug.md) | boolean | Whether to execute expression in \*debug mode\*. In \*debug mode\* inputs and outputs as well as all resolved arguments and time it took to execute each function are saved and are available for introspection. | | [executor](./kibana-plugin-plugins-expressions-server.executionparams.executor.md) | Executor<any> | | | [expression](./kibana-plugin-plugins-expressions-server.executionparams.expression.md) | string | | +| [params](./kibana-plugin-plugins-expressions-server.executionparams.params.md) | ExpressionExecutionParams | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.context.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.params.md similarity index 65% rename from docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.context.md rename to docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.params.md index 8b9a210416dd6..fec60af33e0c4 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.context.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executionparams.params.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-server.executionparams.md) > [context](./kibana-plugin-plugins-expressions-server.executionparams.context.md) +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionParams](./kibana-plugin-plugins-expressions-server.executionparams.md) > [params](./kibana-plugin-plugins-expressions-server.executionparams.params.md) -## ExecutionParams.context property +## ExecutionParams.params property Signature: ```typescript -context?: ExtraContext; +params: ExpressionExecutionParams; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.createexecution.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.createexecution.md index 8ed228d70ff37..34e920a04fd02 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.createexecution.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.createexecution.md @@ -7,7 +7,7 @@ Signature: ```typescript -createExecution = Record, Input = unknown, Output = unknown>(ast: string | ExpressionAstExpression, context?: ExtraContext, { debug }?: ExpressionExecOptions): Execution; +createExecution(ast: string | ExpressionAstExpression, params?: ExpressionExecutionParams): Execution; ``` ## Parameters @@ -15,10 +15,9 @@ createExecution = Recordstring | ExpressionAstExpression | | -| context | ExtraContext | | -| { debug } | ExpressionExecOptions | | +| params | ExpressionExecutionParams | | Returns: -`Execution` +`Execution` 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 46ad60ae07126..97bb3ac895084 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 @@ -29,7 +29,7 @@ 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) | | | @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; ``` ## Parameters @@ -18,8 +18,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | -| context | ExtraContext | | -| options | ExpressionExecOptions | | +| params | ExpressionExecutionParams | | Returns: diff --git a/docs/discover/images/Discover-Start.png b/docs/discover/images/Discover-Start.png index fb885c20c1cf7..12ec2f9889bbd 100644 Binary files a/docs/discover/images/Discover-Start.png and b/docs/discover/images/Discover-Start.png differ diff --git a/docs/discover/images/time-filter.png b/docs/discover/images/time-filter.png new file mode 100644 index 0000000000000..f6d1d5809d7eb Binary files /dev/null and b/docs/discover/images/time-filter.png differ diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index f306f2b8f763f..35f1160ee834d 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -115,7 +115,7 @@ KQL supports `>`, `>=`, `<`, and `<=`. For example: [source,yaml] ------------------- -account_number:>=100 and items_sold:<=200 +account_number >= 100 and items_sold <= 200 ------------------- [discrete] diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index ee1e1526f9d6f..3720a5b457d84 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -104,9 +104,7 @@ To save the current search: . Click *Save* in the Kibana toolbar. . Enter a name for the search and click *Save*. -To import, export and delete saved searches: -. Open the menu, then click *Stack Management. -. From the {kib} menu, click *Saved Ojbects*. +To import, export, and delete saved searches, open the main menu, then click *Stack Management > Saved Ojbects*. ==== Open a saved search To load a saved search into Discover: diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc index 93fdf9ffd695a..dcdc8ee791e83 100644 --- a/docs/discover/set-time-filter.asciidoc +++ b/docs/discover/set-time-filter.asciidoc @@ -30,7 +30,7 @@ to the last 15 minutes. * *Refresh every* to specify an automatic refresh rate. + [role="screenshot"] -image::images/Timepicker-View.png[Time filter menu] +image::images/time-filter.png[Time filter menu] . To set the start and end times, click the bar next to the time filter. In the popup, select *Absolute*, *Relative* or *Now*, then specify the required diff --git a/docs/fleet/images/fleet-start.png b/docs/fleet/images/fleet-start.png index 60e5416fde127..0d0f7b8feec9c 100644 Binary files a/docs/fleet/images/fleet-start.png and b/docs/fleet/images/fleet-start.png differ diff --git a/docs/getting-started/images/add-sample-data.png b/docs/getting-started/images/add-sample-data.png index b8c2002b9c4cd..9dee27dcde71b 100644 Binary files a/docs/getting-started/images/add-sample-data.png and b/docs/getting-started/images/add-sample-data.png differ diff --git a/docs/getting-started/images/tutorial-sample-dashboard.png b/docs/getting-started/images/tutorial-sample-dashboard.png index 9f287640f201c..4c95c04c5e43e 100644 Binary files a/docs/getting-started/images/tutorial-sample-dashboard.png and b/docs/getting-started/images/tutorial-sample-dashboard.png differ diff --git a/docs/getting-started/images/tutorial-sample-filter.png b/docs/getting-started/images/tutorial-sample-filter.png index 7c1d041448557..56ebacadbef45 100644 Binary files a/docs/getting-started/images/tutorial-sample-filter.png and b/docs/getting-started/images/tutorial-sample-filter.png differ diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 6386feac5ab49..f239b7ae6ca88 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -10,8 +10,9 @@ When you've finished, you'll know how to: * <> [float] -=== Before you begin -When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. For more information, refer to {ref}/security-privileges.html[Security privileges]. +=== Required privileges +When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices. +For more information, refer to {ref}/security-privileges.html[Security privileges]. [float] [[set-up-on-cloud]] @@ -30,7 +31,7 @@ Sample data sets come with sample visualizations, dashboards, and more to help y . On the *Sample eCommerce orders* card, click *Add data*. + [role="screenshot"] -image::getting-started/images/add-sample-data.png[] +image::getting-started/images/add-sample-data.png[Add data UI] [float] [[explore-the-data]] @@ -38,7 +39,7 @@ image::getting-started/images/add-sample-data.png[] *Discover* displays an interactive histogram that shows the distribution of of data, or documents, over time, and a table that lists the fields for each document that matches the index. By default, all fields are shown for each matching document. -. Open the menu, then click *Discover*. +. Open the main menu, then click *Discover*. . Change the <> to *Last 7 days*. + @@ -70,7 +71,7 @@ For more information, refer to <>. A dashboard is a collection of panels that you can use to view and analyze the data. Panels contain visualizations, interactive controls, Markdown, and more. -. Open the menu, then click *Dashboard*. +. Open the main menu, then click *Dashboard*. . Click *[eCommerce] Revenue Dashboard*. + @@ -83,7 +84,7 @@ image::getting-started/images/tutorial-sample-dashboard.png[] To focus in on the data you want to view on the dashboard, use filters. -. From the *Controls* visualization, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. +. From the *[eCommerce] Controls* panel, make a selection from the *Manufacturer* and *Category* dropdowns, then click *Apply changes*. + For example, the following dashboard shows the data for women's clothing from Gnomehouse. + @@ -103,11 +104,11 @@ For more information, refer to <>. [float] [[create-a-visualization]] -=== Create a visualization +=== Create a visualization panel -To create a treemap that shows the top regions and manufacturers, use *Lens*, then add the treemap to the dashboard. +To create a treemap panel that shows the top regions and manufacturers, use *Lens*, then add the treemap panel to the dashboard. -. From the {kib} toolbar, click *Edit*, then click *Create new*. +. From the toolbar, click *Edit*, then click *Create new*. . On the *New Visualization* window, click *Lens*. @@ -126,7 +127,7 @@ image::getting-started/images/tutorial-visualization-dropdown.png[Visualization . On the *Save Lens visualization*, enter a title and make sure *Add to Dashboard after saving* is selected, then click *Save and return*. + -The treemap appears as the last visualization on the dashboard. +The treemap appears as the last visualization panel on the dashboard. + [role="screenshot"] image::getting-started/images/tutorial-final-dashboard.gif[Final dashboard with new treemap visualization] diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 8e8d0e5bf996e..293597685ecc0 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -6,7 +6,7 @@ behavior of Kibana. For example, you can change the format used to display dates specify the default index pattern, and set the precision for displayed decimal values. -. Open the menu, then go to *Stack Management > {kib} > Advanced Settings*. +. Open the main menu, then click *Stack Management > Advanced Settings*. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. . Click *Save changes*. diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc index 429d7915cc1c3..0c7ca7f1db17d 100644 --- a/docs/management/alerting/alerts-and-actions-intro.asciidoc +++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc @@ -6,8 +6,8 @@ beta[] The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing. -To manage alerting and connectors, open the menu, -then go to *Stack Management > Alerts and Insights > Alerts and Actions*. +To manage alerting and connectors, open the main menu, +then click *Stack Management > Alerts and Insights > Alerts and Actions*. [role="screenshot"] image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI] diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 7de2a042160e9..e83e6d262f26c 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -25,8 +25,8 @@ image::images/management-index-read-only-badge.png[Example of Index Pattern Mana [[settings-create-pattern]] === Create an index pattern -When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the menu, -then go to *Stack Management > {kib} > Index Patterns* to go directly to the *Index Patterns* UI. +When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the main menu, +then click *Stack Management > Index Patterns*. [role="screenshot"] image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollup index pattern"] diff --git a/docs/management/index-patterns/images/index-pattern-ui.png b/docs/management/index-patterns/images/index-pattern-ui.png new file mode 100644 index 0000000000000..7d16540aa03a2 Binary files /dev/null and b/docs/management/index-patterns/images/index-pattern-ui.png differ diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc index 7986e4e56279a..d9745bfef524a 100644 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc @@ -7,7 +7,7 @@ pipelines that perform common transformations and enrichments on your data. For example, you might remove a field, rename an existing field, or set a new field. -You’ll find *Ingest Node Pipelines* in *Stack Management > Ingest*. With this feature, you can: +To begin, open the main menu, then click *Stack Management > Ingest Node Pipelines*. With *Ingest Node Pipelines*, you can: * View a list of your pipelines and drill down into details. * Create a pipeline that defines a series of tasks, known as processors. @@ -23,7 +23,7 @@ image:management/ingest-pipelines/images/ingest-pipeline-list.png["Ingest node p The minimum required permissions to access *Ingest Node Pipelines* are the `manage_pipeline` and `cluster:monitor/nodes/info` cluster privileges. -You can add these privileges in *Stack Management > Security > Roles*. +To add privileges, open the main menu, then click *Stack Management > Roles*. [role="screenshot"] image:management/ingest-pipelines/images/ingest-pipeline-privileges.png["Privileges required for Ingest Node Pipelines"] diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc index 678e160b99af0..10c98cca26345 100644 --- a/docs/management/managing-beats.asciidoc +++ b/docs/management/managing-beats.asciidoc @@ -4,7 +4,7 @@ include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued] -To use {beats} Central Management UI, open the menu, go to *Stack Management > Ingest > +To use {beats} Central Management, open the main menu, click *Stack Management > {beats} Central Management*, then define and manage configurations in a central location in {kib} and quickly deploy configuration changes to all {beats} running across your enterprise. For more @@ -18,8 +18,8 @@ about central management, see the related {beats} documentation: This feature requires an Elastic license that includes {beats} central management. -Don't have a license? You can start a 30-day trial. Open the menu, -go to *Stack Management > Stack > License Management*. At the end of the trial +Don't have a license? You can start a 30-day trial. Open the main menu, then +click *Stack Management > License Management*. At the end of the trial period, you can purchase a subscription to keep using central management. For more information, see https://www.elastic.co/subscriptions and <>. diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index ad3a0ef0fcdd1..441bce43c7cdf 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -134,7 +134,7 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless [[create-scripted-field]] === Create a scripted field -. Open the menu, then go to *Stack Management > {kib} > Index Patterns* +. Open the main menu, then click *Stack Management > Index Patterns*. . Select the index pattern you want to add a scripted field to. . Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. . Enter a name for the scripted field. diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index b199e076443ab..8416c164c6c51 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -12,15 +12,7 @@ way possible. This page shows you how to use *Index Management* features to: -* View and edit index settings. -* View mappings and statistics for an index. -* Perform index-level operations, such as refreshes and freezes. -* View and manage data streams. -* Create index templates to automatically configure new data streams and -indices. - -To manage your indices, open the menu, then click *Stack Management > Index -Management*. +To manage your indices, open the main menu, then click *Stack Management > Index Management*. [role="screenshot"] image::images/management_index_labels.png[Index Management UI] diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index b53bda95466dc..8944414f6bfbc 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -7,7 +7,7 @@ with no expiration date. For the full list of features, refer to If you want to try out the full set of features, you can activate a free 30-day trial. To view the status of your license, start a trial, or install a new -license, open the menu, then go to *Stack Management > Stack > License Management*. +license, open the main menu, then click *Stack Management > License Management*. NOTE: You can start a trial only if your cluster has not already activated a trial license for the current major product version. For example, if you have @@ -34,7 +34,7 @@ the features that will no longer be supported if you revert to a basic license. The `manage` cluster privilege is required to access *License Management*. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [discrete] [[update-license]] diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 8c885ddca52e5..639be87c540fb 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -5,13 +5,7 @@ The *Saved Objects* UI helps you keep track of and manage your saved objects. Th store data for later use, including dashboards, visualizations, maps, index patterns, Canvas workpads, and more. -To get started, open the menu, then go to *Stack Management > {kib} > Saved Objects*. With this UI, you can: - -* <> -* <> -* <> -* <> - +To get started, open the main menu, then click *Stack Management > Saved Objects*. [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 7324f45594bd7..bc876ab67bc62 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -8,11 +8,7 @@ by an index pattern, and then rolls it into a new index. Rollup indices are a go compactly store months or years of historical data for use in visualizations and reports. -To get started, open the menu, then go to *Stack Management > Data > Rollup Jobs*. With this UI, -you can: - -* <> -* <> +To get started, open the main menu, then click *Stack Management > Rollup Jobs*. [role="screenshot"] image::images/management_rollup_list.png[][List of currently active rollup jobs] @@ -25,7 +21,7 @@ Before using this feature, you should be familiar with how rollups work. The `manage_rollup` cluster privilege is required to access *Rollup jobs*. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [float] [[create-and-manage-rollup-job]] @@ -137,7 +133,7 @@ Your next step is to visualize your rolled up data in a vertical bar chart. Most visualizations support rolled up data, with the exception of Timelion and Vega visualizations. -. Go to *Stack Management > {kib} > Index Patterns*. +. Open the main menu, then click *Stack Management > Index Patterns*. . Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. + @@ -152,7 +148,7 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log matches the rolled up index pattern and `kibana_sample_data_logs` matches the index pattern for raw data. -. Go to *Dashboard* and create a vertical bar chart. +. Open the main menu, click *Dashboard*, then create and add a vertical bar chart. . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc index 1bf62522e245c..62633441ef161 100644 --- a/docs/management/snapshot-restore/index.asciidoc +++ b/docs/management/snapshot-restore/index.asciidoc @@ -8,7 +8,7 @@ Snapshots are important because they provide a copy of your data in case something goes wrong. If you need to roll back to an older version of your data, you can restore a snapshot from the repository. -To get started, open the menu, then go to *Stack Management > Data > Snapshot and Restore*. +To get started, open the main menu, then click *Stack Management > Snapshot and Restore*. With this UI, you can: * Register a repository for storing your snapshots @@ -32,7 +32,7 @@ The minimum required permissions to access *Snapshot and Restore* include: * Cluster privileges: `monitor`, `manage_slm`, `cluster:admin/snapshot`, and `cluster:admin/repository` * Index privileges: `all` on the `monitor` index if you want to access content in the *Restore Status* tab -To add privileges, open the menu, then go to *Stack Management > Security > Roles*. +To add privileges, open the main menu, then click *Stack Management > Roles*. [role="screenshot"] image:management/snapshot-restore/images/snapshot_permissions.png["Edit Role"] @@ -191,7 +191,7 @@ your master and data nodes. You can do this in one of two ways: Use *Snapshot and Restore* to register the repository where your snapshots will live. -. Open the menu, then go to *Stack Management > Data > Snapshot and Restore*. +. Open the main menu, then click *Stack Management > Snapshot and Restore*. . Click *Register a repository* in either the introductory message or *Repository view*. . Enter a name for your repository, for example, `my_backup`. . Select *Shared file system*. @@ -212,7 +212,7 @@ The repository currently doesn’t have any snapshots. ==== Add a snapshot to the repository Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot. -. Open the menu, go to *Dev Tools*, then select *Console*. +. Open the main menu, click *Dev Tools*, then select *Console*. . Create the snapshot: + [source,js] diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index 2b8c2da2ef577..61df6457a9bde 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -4,7 +4,7 @@ The Upgrade Assistant helps you prepare for your upgrade to the next major {es} version. For example, if you are using 6.8, the Upgrade Assistant helps you to upgrade to 7.0. -To access the assistant, open the menu, then go to *Stack Management > Stack > Upgrade Assistant*. +To access the assistant, open the main menu, then click *Stack Management > Upgrade Assistant*. The assistant identifies the deprecated settings in your cluster and indices and guides you through the process of resolving issues, including reindexing. @@ -19,7 +19,7 @@ For example, if you want to upgrade to to 7.0, make sure that you are using 6.8. The `manage` cluster privilege is required to access the *Upgrade assistant*. Additional privileges may be needed to perform certain actions. -You can add this privilege in *Stack Management > Security > Roles*. +To add the privilege, open the main menu, then click *Stack Management > Roles*. [float] === Reindexing diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 23a0acbff5718..69c33aa7a1dac 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -8,8 +8,8 @@ Watches are helpful for analyzing mission-critical and business-critical streaming data. For example, you might watch application logs for performance outages or audit access logs for security threats. -To get started with the Watcher UI, open then menu, -then go to *Stack Management > Alerts and Insights > Watcher*. +To get started, open then main menu, +then click *Stack Management > Watcher*. With this UI, you can: * <> @@ -41,7 +41,7 @@ and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. * `watcher_user`. You can view watches, but not create or edit them. -To manage roles, open then menu, then go to *Stack Management > Security > Roles*, or use the +To manage roles, open then main menu, then click *Stack Management > Roles*, or use the <>. Watches are shared between all users with the same role. diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc new file mode 100644 index 0000000000000..3c9bea11176cc --- /dev/null +++ b/docs/maps/geojson-upload.asciidoc @@ -0,0 +1,44 @@ +[role="xpack"] +[[geojson-upload]] +== Upload GeoJSON data + +Maps makes it easy to import geospatial data into the Elastic Stack. +Using the GeoJSON Upload feature, you can drag and drop your point and shape +data files directly into {es}, and then use them as layers +in the map. You can also use the GeoJSON data in the broader Kibana ecosystem, +for example, in visualizations and Canvas workpads. + +[float] +=== Why GeoJSON? +GeoJSON is an open-standard file format for storing geospatial vector data. +Although many vector data formats are available in the GIS community, +GeoJSON is the most commonly used and flexible option. +[float] + +=== Upload a GeoJSON file +Follow these instructions to upload a GeoJSON data file, or try the +<>. + +. Open the main menu, click *Maps*, and then click *Add layer*. +. Click *Uploaded GeoJSON*. ++ +[role="screenshot"] +image::maps/images/fu_gs_select_source_file_upload.png[] + +. Use the file chooser to select a valid GeoJSON file. The file will load +a preview of the data on the map. +. Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data, +or override it and select {ref}/geo-shape.html[geo_shape]. +All other shapes will default to a type of `geo_shape`. +. Leave the default *Index name* and *Index pattern* names (the name of the uploaded +file minus its extension). You might need to change the index name if it is invalid. +. Click *Import file*. ++ +Upon completing the indexing process and creating the associated index pattern, +the Elasticsearch responses are shown on the *Layer add panel* and the indexed data +appears on the map. The geospatial data on the map +should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch. + +. To continue adding data to the map, click *Add layer*. +. In *Layer settings*, adjust any settings or <> as needed. +. Click *Save & close*. diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index 194d09c491cee..ff0c9bf1f72ba 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -11,7 +11,7 @@ Choose an import tool based on the format of your geospatial data. *File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point. -. Open the side navigation menu, and click *Machine Learning*. +. Open the main menu, then click *Machine Learning*. . Select the *Data Visualizer* tab, then click *Upload file*. . Use the file chooser to select a CSV file. . Click *Import*. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index f48ff268755d2..5c6cd87b235e1 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -50,7 +50,7 @@ In this tutorial, you'll learn to: The first thing to do is to create a new map. -. If you haven't already, click *{kib} > Maps* from the side navigation. +. If you haven't already, open the main menu, then click *Maps*. . On the maps list page, click *Create map*. . Set the time range to *Last 7 days*. + @@ -188,7 +188,7 @@ You have completed the steps for re-creating the sample data map. === Add the map to a dashboard You can add your saved map to a {kibana-ref}/dashboard.html[dashboard] and view your geospatial data alongside bar charts, pie charts, and other visualizations. -. Open the menu, then go to *Dashboard*. +. Open the main menu, then click *Dashboard*. . Click *Create dashboard*. . Set the time range to *Last 7 days*. . Click *Add*. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index b396c40aa21f9..9054a97c90496 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -18,7 +18,7 @@ It is enabled by default. // Any changes made in this file will be seen there as well. // tag::apm-indices-settings[] -Index defaults can be changed in Kibana. Navigate to *APM* > *Settings* > *Indices*. +Index defaults can be changed in Kibana. Open the main menu, then click *APM > Settings > Indices*. Index settings in the APM app take precedence over those set in `kibana.yml`. [role="screenshot"] @@ -44,7 +44,7 @@ Changing these settings may disable features of the APM App. | Set to `false` to disable the APM app. Defaults to `true`. | `xpack.apm.ui.enabled` {ess-icon} - | Set to `false` to hide the APM app from the menu. Defaults to `true`. + | Set to `false` to hide the APM app from the main menu. Defaults to `true`. | `xpack.apm.ui.transactionGroupBucketSize` | Number of top transaction groups displayed in the APM app. Defaults to `1000`. diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 917821ad09e2f..f48dbeab9d61a 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -76,6 +76,9 @@ a|`monitoring.cluster_alerts.` health checks. By default, it matches the <> setting, which has a default value of `30000`. +| `monitoring.ui.elasticsearch.ssl` + | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster. + |=== [float] diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc index 49aa411e91512..edf936fe54267 100644 --- a/docs/setup/access.asciidoc +++ b/docs/setup/access.asciidoc @@ -1,24 +1,37 @@ [[access]] == Access {kib} -Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the -machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. -If you want to allow remote users to connect, set the parameter `server.host` in `kibana.yml` to a non-loopback address. +The fastest way to access {kib} is to use our hosted {es} Service. If you <>, access {kib} through the web application. -When you access Kibana, the <> page loads by default with the default index pattern selected. The -time filter is set to the last 15 minutes and the search query is set to match-all (\*). +[float] +=== Set up on cloud -If you don't see any documents, try setting the time filter to a wider time range. -If you still don't see any results, it's possible that you don't *have* any documents. +include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] + +[float] +[[log-on-to-the-web-application]] +=== Log on to the web application + +If you are using a self-managed deployment, you access {kib} through the web application on port 5601. + +. Point your web browser to the machine where you are running {kib} and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. + +. To allow remote users to connect to {kib}, set the parameter `server.host` in kibana.yml to a non-loopback address. + +. On the home page, click *{kib}*. ++ +To make the {kib} page your landing page, click *Make this my landing page*. [float] [[status]] -=== Check {kib} status +=== Check the {kib} status -You can reach the Kibana server's status page by navigating to the status endpoint, for example, `localhost:5601/status`. The status page displays -information about the server's resource usage and lists the installed plugins. +To view the {kib} status page, use the status endpoint. For example, `localhost:5601/status`. The status page displays +information about the server resource usage and installed plugins. [role="screenshot"] image::images/kibana-status-page-7_5_0.png[] -NOTE: For JSON-formatted server status details, use the API endpoint at `localhost:5601/api/status` +For JSON-formatted server status details, use the `localhost:5601/api/status` API endpoint. + + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 3db562319641c..c968ca6f35029 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -20,14 +20,15 @@ to see all that you can do in {kib}. experimental[] -To visualize data in a CSV, JSON, or log file, you can upload it using the File -Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and -then drag your file into the File Data Visualizer. Alternatively, you can open -it by navigating to *Machine Learning* from the side navigation and selecting +To visualize data in a CSV, JSON, or log file, you can upload it using the File +Data Visualizer. On the home page, click *Upload a file*, and +then drag your file onto the *File Data Visualizer*. Alternatively, you can open +it by navigating to *Machine Learning* from the side navigation and selecting + *Data Visualizer*. [role="screenshot"] -image::images/data-viz-homepage.jpg[File Data Visualizer on the home page] +image::images/ingest-data.png[File Data Visualizer on the home page] You can upload a file up to 100 MB. This value is configurable up to 1 GB in <>. @@ -78,7 +79,7 @@ create an index pattern that matches the names of the indices that you want to e When you add data with the File Data Visualizer, GeoJSON Upload feature, or built-in tutorial, an index pattern is created for you. -. Go to *Stack Management*, and then click *Index Patterns*. +. Open the main menu, then click *Stack Management > Index Patterns*. . Click *Create index pattern*. diff --git a/docs/setup/images/data-viz-homepage.jpg b/docs/setup/images/data-viz-homepage.jpg deleted file mode 100644 index f7a952b65d41f..0000000000000 Binary files a/docs/setup/images/data-viz-homepage.jpg and /dev/null differ diff --git a/docs/setup/images/ingest-data.png b/docs/setup/images/ingest-data.png new file mode 100644 index 0000000000000..b1943d6de27d2 Binary files /dev/null and b/docs/setup/images/ingest-data.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 9e505b8bfe045..1bc781e1dda49 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -29,7 +29,7 @@ Kibana supports spaces in several ways. You can: [[spaces-managing]] === View, create, and delete spaces -Open the menu, then go to *Stack Management > {kib} > Spaces* for an overview of your spaces. This view provides actions +Open the main menu, then click *Stack Management > Spaces* for an overview of your spaces. This view provides actions for you to create, edit, and delete spaces. [role="screenshot"] @@ -94,8 +94,8 @@ image::spaces/images/spaces-roles.png["Controlling features visiblity"] [[spaces-moving-objects]] === Move saved objects between spaces -To <> from one space to another, open the menu, -then go to *Stack Management > {kib} > Saved objects*. +To <> from one space to another, open the main menu, +then click *Stack Management > Saved Objects*. Alternately, you can move objects using {kib}'s <> interface. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 9301224e6df48..aad192dbddb30 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -89,8 +89,8 @@ image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab + * Create a connector as part of creating an alert by selecting PagerDuty in the *Actions* section of the alert configuration and selecting *Add new*. -* Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting -*Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option. +* Alternatively, create a connector. To create a connector, open the main menu, click *Stack Management* > +Alerts and Actions*, select *Connectors*, click *Create connector*, then select the PagerDuty option. . Configure the connector by giving it a name and entering the Integration Key, optionally entering a custom API URL. + @@ -99,7 +99,7 @@ See <> for how to obtain the endpoint and . Save the Connector. -. Create an alert using *Management > Alerts and Actions* or the application of your choice. +. To create an alert, open the main menu, then click *Stack Management > Alerts and Actions* or the application of your choice. . Set up an action using your PagerDuty connector, by determining: + @@ -120,7 +120,7 @@ To remove a PagerDuty connector from an alert, simply remove it from the *Actions* section of that alert, using the remove (x) icon. This will disable the integration for the particular alert. -To delete the connector entirely, go to *Management > Alerts and Actions*. +To delete the connector entirely, open the main menu, then click *Stack Management > Alerts and Actions*. Select the *Connectors* tab, and then click on the delete icon. This is an irreversible action and impacts all alerts that use this connector. diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index e3f1703f08e88..722607ac05f87 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -61,7 +61,7 @@ Sensitive properties, such as passwords, can also be stored in the < {kib} > Alerts and Actions*, preconfigured connectors +When you open the main menu, click *Stack Management > Alerts and Actions*. Preconfigured connectors appear on the <>, regardless of which space you are in. They are tagged as “preconfigured”, and you cannot delete them. @@ -101,7 +101,7 @@ This example shows a preconfigured action type with one out-of-the box connector [[managing-pre-configured-action-types]] To attach a preconfigured action to an alert: -. Open the menu, then go to *Stack Management > {kib} > Alerts and Actions*, open the *Connectors* tab. +. Open the main menu, click *Stack Management > Alerts and Actions*, then open the *Connectors* tab. . Click *Create connector.* diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 297dfac5b10bd..c10641bb3a6b9 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -17,7 +17,7 @@ With Canvas, you can: * Focus the data you want to display with filters. -To begin, open the menu, then go to *Canvas*. +To begin, open the main menu, then click *Canvas*. [role="screenshot"] image::images/canvas-gs-example.png[Getting started example] diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc index 5e928fd731bb4..bdff7355d7467 100644 --- a/docs/user/dashboard/dashboard-drilldown.asciidoc +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -57,8 +57,8 @@ TIP: If you don’t see data for a panel, try changing the time range. . Set a search and filter. + [%hardbreaks] -Search: `extension.keyword:( “gz” or “css” or “deb”)` -Filter: `geo.src : CN` +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` *Create the drilldown* @@ -94,4 +94,3 @@ image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to an + You are navigated to your destination dashboard. Verify that the search query, filters, and time range are carried over. - diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 4fa4f9860c2bd..5fda1af55c7fe 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -8,7 +8,7 @@ A _dashboard_ is a collection of panels that you use to analyze your data. On a you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, interactive controls, markdown, and more. -With *Dashboard*s, you can: +With *Dashboard*, you can: * Add multiple panels to see many aspects and views of your data in one place. @@ -22,7 +22,7 @@ With *Dashboard*s, you can: * Generate reports based on your findings. -To begin, open the menu, go to *Dashboard*, then click *Create dashboard*. +To begin, open the main menu, click *Dashboard*, then click *Create dashboard*. [role="screenshot"] image:images/Dashboard_example.png[Example dashboard] @@ -424,33 +424,37 @@ Ready to try out Timelion? For step-by-step tutorials, refer to: [[timelion-deprecation]] ==== Timelion app deprecation -Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard. +In 7.0 and later, *Timelion* app is deprecated. In 8.0 and later, *Timelion* app is removed from {kib}. To prepare for the removal of *Timelion* app, you must migrate *Timelion* app worksheets to a dashboard. -NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion -visualizations on dashboards and in Visualize and Canvas. +NOTE: Only *Timelion* app is deprecated. {kib} continues to support *Timelion* +visualizations in *Dashboard*, *Visualize*, and *Canvas*. -To migrate a Timelion worksheet to a dashboard: +To migrate a *Timelion* worksheet to a dashboard: -. Open the menu, click **Dashboard**, then click **Create dashboard**. +. Open the main menu, click *Dashboard*, then click *Create dashboard*. -. On the dashboard, click **Create New**, then select the Timelion visualization. +. For each *Timelion* app worksheet, complete the following steps. -. On a new tab, open the Timelion app, select the chart you want to copy, and copy its expression. +.. On the dashboard, click *Create New*, then click *Timelion* on the *New Visualization* window. + +.. Open a new tab, open the *Timelion* app, select the chart you want to copy, then copy the chart expression. + [role="screenshot"] -image::images/timelion-copy-expression.png[] +image::images/timelion-copy-expression.png[Timelion app chart] -. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**. +.. Go to *Timelion*, paste the chart expression in the *Timelion expression* field, then click *Update*. + [role="screenshot"] -image::images/timelion-vis-paste-expression.png[] +image::images/timelion-vis-paste-expression.png[Timelion advanced editor UI] + +.. In the toolbar, click *Save*. -. Save the new visualization, give it a name, and click **Save and Return**. +.. On the *Save visualization* window, enter the visualization *Title*, then click *Save and return*. + -Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet. +The Timelion visualization panel appears on the dashboard. + [role="screenshot"] -image::images/timelion-dashboard.png[] +image::images/timelion-dashboard.png[Final dashboard with saved Timelion app worksheet] [float] [[save-panels]] @@ -458,7 +462,7 @@ image::images/timelion-dashboard.png[] When you’ve finished making changes, save the panels. -. Click *Save*. +. In the toolbar, click *Save*. . Add the *Title* and optional *Description*. . Click *Save and return*. diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc index 7534ea1e9e9fb..7b712b355b315 100644 --- a/docs/user/dashboard/edit-dashboards.asciidoc +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -78,7 +78,7 @@ Put the dashboard in *Edit* mode, then use the following options: * To resize, click the resize control, then drag to the new dimensions. -* To delete, open the panel menu, then select Delete from dashboard. When you delete a panel from the dashboard, the +* To delete, open the panel menu, then select *Delete from dashboard*. When you delete a panel from the dashboard, the visualization or saved search from the panel is still available in Kibana. [float] diff --git a/docs/user/dashboard/images/Dashboard_add_new_visualization.png b/docs/user/dashboard/images/Dashboard_add_new_visualization.png index 3685f9c5c9a74..5f73b2f1adde2 100644 Binary files a/docs/user/dashboard/images/Dashboard_add_new_visualization.png and b/docs/user/dashboard/images/Dashboard_add_new_visualization.png differ diff --git a/docs/user/dashboard/images/Dashboard_add_visualization.png b/docs/user/dashboard/images/Dashboard_add_visualization.png index b1b86d47e5982..4caa34ef3d082 100644 Binary files a/docs/user/dashboard/images/Dashboard_add_visualization.png and b/docs/user/dashboard/images/Dashboard_add_visualization.png differ diff --git a/docs/user/dashboard/images/Dashboard_example.png b/docs/user/dashboard/images/Dashboard_example.png index 1a80f4b3bdf07..c2e338d0fd31b 100644 Binary files a/docs/user/dashboard/images/Dashboard_example.png and b/docs/user/dashboard/images/Dashboard_example.png differ diff --git a/docs/user/dashboard/images/Dashboard_inspect.png b/docs/user/dashboard/images/Dashboard_inspect.png index d65b968e043a6..635eef4a017f6 100644 Binary files a/docs/user/dashboard/images/Dashboard_inspect.png and b/docs/user/dashboard/images/Dashboard_inspect.png differ diff --git a/docs/user/dashboard/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif index c9b3311df0325..c438e14371887 100644 Binary files a/docs/user/dashboard/images/drilldown_on_piechart.gif and b/docs/user/dashboard/images/drilldown_on_piechart.gif differ diff --git a/docs/user/dashboard/images/timelion-copy-expression.png b/docs/user/dashboard/images/timelion-copy-expression.png new file mode 100644 index 0000000000000..a9c3afe9b060f Binary files /dev/null and b/docs/user/dashboard/images/timelion-copy-expression.png differ diff --git a/docs/visualize/images/timelion-vis-paste-expression.png b/docs/user/dashboard/images/timelion-vis-paste-expression.png similarity index 100% rename from docs/visualize/images/timelion-vis-paste-expression.png rename to docs/user/dashboard/images/timelion-vis-paste-expression.png diff --git a/docs/user/dashboard/images/url_drilldown_go_to_github.gif b/docs/user/dashboard/images/url_drilldown_go_to_github.gif index 7cca3f72d5a68..3a3b00dc0e2ce 100644 Binary files a/docs/user/dashboard/images/url_drilldown_go_to_github.gif and b/docs/user/dashboard/images/url_drilldown_go_to_github.gif differ diff --git a/docs/user/dashboard/share-dashboards.asciidoc b/docs/user/dashboard/share-dashboards.asciidoc index cfa146d60fdac..6c05240c934e8 100644 --- a/docs/user/dashboard/share-dashboards.asciidoc +++ b/docs/user/dashboard/share-dashboards.asciidoc @@ -23,5 +23,5 @@ tools. To create a short URL, you must have write access to {kib}. [[import-dashboards]] === Export the dashboard -To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, +To export the dashboard, open the main menu, then click *Stack Management > Saved Objects*. For more information, refer to <>. \ No newline at end of file diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index aca6d40a3532e..086c0707b3c2c 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -9,7 +9,7 @@ You must index data into {es} before you can create a graph. [[exploring-connections]] === Graph a data connection -. Open the menu, then go to *Graph*. +. Open the main menu, then click *Graph*. + If this is your first graph, follow the prompts to create it. For subsequent graphs, click *New*. diff --git a/docs/user/graph/images/graph-add-query.png b/docs/user/graph/images/graph-add-query.png index 1b233e3ef8b69..93ddf6a6132f4 100644 Binary files a/docs/user/graph/images/graph-add-query.png and b/docs/user/graph/images/graph-add-query.png differ diff --git a/docs/user/graph/images/graph-link-summary.png b/docs/user/graph/images/graph-link-summary.png index 4c75be00de0f5..a3dfdc0f79d96 100644 Binary files a/docs/user/graph/images/graph-link-summary.png and b/docs/user/graph/images/graph-link-summary.png differ diff --git a/docs/user/graph/images/graph-url-connections.png b/docs/user/graph/images/graph-url-connections.png index 4f8c163ab764b..34b57d489b048 100644 Binary files a/docs/user/graph/images/graph-url-connections.png and b/docs/user/graph/images/graph-url-connections.png differ diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 7e5dc59b03a2c..aa5b0ece08db7 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -20,16 +20,16 @@ and more — all from the convenience of a {kib} UI. document discovery to SIEM, {kib} is the portal for accessing these and other capabilities. [role="screenshot"] -image::images/intro-kibana.png[] +image::images/intro-kibana.png[Kibana home page] [float] [[get-data-into-kibana]] -=== Add data +=== Ingest data -{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores +{kib} is designed to use {es} as a data source. Think of {es} as the engine that stores and processes the data, with {kib} sitting on top. -From the home page, {kib} provides these options for adding data: +From the home page, {kib} provides these options for ingesting data: * Import data using the https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[File Data visualizer]. @@ -60,7 +60,7 @@ search for hidden insights and relationships. Ask your questions, and then narrow the results to just the data you want. [role="screenshot"] -image::images/intro-discover.png[] +image::images/intro-discover.png[Discover UI] [float] [[visualize-and-analyze]] @@ -79,7 +79,7 @@ use <> to collect them in one place. A dashboard provides insights into your data from multiple perspectives. [role="screenshot"] -image::images/intro-dashboard.png[] +image::images/intro-dashboard.png[Sample eCommerce data set dashboard] {kib} also offers these visualization features: @@ -156,5 +156,4 @@ You can also <> — no code, no addi infrastructure required. Our <> and in-product guidance can -help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] -in the top navigation bar for help with questions or to provide feedback. +help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] for help with questions or to provide feedback. diff --git a/docs/user/introduction/images/intro-dashboard.png b/docs/user/introduction/images/intro-dashboard.png index fe4e6f620d19c..bb4e98a516fb7 100644 Binary files a/docs/user/introduction/images/intro-dashboard.png and b/docs/user/introduction/images/intro-dashboard.png differ diff --git a/docs/user/introduction/images/intro-data-tutorial.png b/docs/user/introduction/images/intro-data-tutorial.png index 2882a092fbb0b..781e134605b87 100644 Binary files a/docs/user/introduction/images/intro-data-tutorial.png and b/docs/user/introduction/images/intro-data-tutorial.png differ diff --git a/docs/user/introduction/images/intro-discover.png b/docs/user/introduction/images/intro-discover.png index 54e5725596421..134804941a356 100644 Binary files a/docs/user/introduction/images/intro-discover.png and b/docs/user/introduction/images/intro-discover.png differ diff --git a/docs/user/introduction/images/intro-kibana.png b/docs/user/introduction/images/intro-kibana.png index 62c2c99826131..3d10a31d7e380 100644 Binary files a/docs/user/introduction/images/intro-kibana.png and b/docs/user/introduction/images/intro-kibana.png differ diff --git a/docs/user/introduction/images/intro-spaces.png b/docs/user/introduction/images/intro-spaces.png new file mode 100644 index 0000000000000..6f3212cbde26e Binary files /dev/null and b/docs/user/introduction/images/intro-spaces.png differ diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 1ac5c385f8ed5..300497126c3e5 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -31,6 +31,33 @@ default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running checks on a schedule time of 1 minute with a re-notify internal of 1 day. +[discrete] +[[kibana-alerts-disk-usage-threshold]] +== Disk usage threshold + +This alert is triggered when a node is nearly at disk capacity. By +default, the trigger condition is set at 80% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-jvm-memory-threshold]] +== JVM memory threshold + +This alert is triggered when a node runs a consistently high JVM memory usage. By +default, the trigger condition is set at 85% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-missing-monitoring-data]] +== Missing monitoring data + +This alert is triggered when any stack products nodes or instances stop sending +monitoring data. By default, the trigger condition is set to missing for 15 minutes +looking back 1 day. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 6 hours. + NOTE: Some action types are subscription features, while others are free. For a comparison of the Elastic subscription levels, see the alerting section of the {subscriptions}[Subscriptions page]. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index 9d735ea1fe3db..047fcc08775e6 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -48,7 +48,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`. If {security-features} are enabled, log in. -- -... Open the menu, then go to *Stack Monitoring*. If data collection is +... Open the main menu, then click *Stack Monitoring*. If data collection is disabled, you are prompted to turn it on. ** From the Console or command line, set `xpack.monitoring.collection.enabled` diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 0c48e3b7d011d..9507b70c4f72e 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -80,7 +80,7 @@ By default, if you are running {kib} locally, go to `http://localhost:5601/`. If the Elastic {security-features} are enabled, log in. -- -. Open *Stack Monitoring*. +. Open the main menu, then click *Stack Monitoring*. + -- If data collection is disabled, you are prompted to turn on data collection. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 371855deb2f3c..413573e7ec182 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -13,7 +13,7 @@ URL that triggers a report to generate. To create the POST URL for PDF reports: -. Go to *Dashboard*, then open the visualization or dashboard. +. Open then main menu, click *Dashboard*, then open a dashboard. + To specify a relative or absolute time period, use the time filter. diff --git a/docs/user/reporting/images/preserve-layout-switch.png b/docs/user/reporting/images/preserve-layout-switch.png index 9cfbdaafc3ac5..0aaefb14d7ee5 100644 Binary files a/docs/user/reporting/images/preserve-layout-switch.png and b/docs/user/reporting/images/preserve-layout-switch.png differ diff --git a/docs/user/reporting/images/share-button.png b/docs/user/reporting/images/share-button.png deleted file mode 100644 index 0b307d947935e..0000000000000 Binary files a/docs/user/reporting/images/share-button.png and /dev/null differ diff --git a/docs/user/reporting/images/share-menu.png b/docs/user/reporting/images/share-menu.png new file mode 100644 index 0000000000000..7f1d9eda0b5bc Binary files /dev/null and b/docs/user/reporting/images/share-menu.png differ diff --git a/docs/user/reporting/images/shareable-container.png b/docs/user/reporting/images/shareable-container.png index e114f63e2fe12..829fe15706a52 100644 Binary files a/docs/user/reporting/images/shareable-container.png and b/docs/user/reporting/images/shareable-container.png differ diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 50ae92382fb24..cd93389bb5fde 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -14,7 +14,7 @@ Reporting is available from the *Share* menu in *Discover*, *Dashboard*, and *Canvas*. [role="screenshot"] -image::user/reporting/images/share-button.png["Share"] +image::user/reporting/images/share-menu.png["Share"] [float] == Setup @@ -94,7 +94,7 @@ image::user/reporting/images/preserve-layout-switch.png["Share"] [[manage-report-history]] == View and manage report history -For a list of your reports, open the menu, then go to *Stack Management > Alerts and Insights > Reporting*. +For a list of your reports, open the main menu, then click *Stack Management > Reporting*. From this view, you can monitor the generation of a report and download reports that you previously generated. diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc index 7cf1b964082d9..8b59115859622 100644 --- a/docs/user/security/api-keys/index.asciidoc +++ b/docs/user/security/api-keys/index.asciidoc @@ -15,7 +15,7 @@ Or, you might create API keys to automate ingestion of new data from remote sources, without a live user interaction. You can create API keys from the {kib} Console. To view and invalidate -API keys, open the menu, then go to *Stack Management > Security > API Keys*. +API keys, open the main menu, then click *Stack Management > API Keys*. [role="screenshot"] image:user/security/api-keys/images/api-keys.png["API Keys UI"] @@ -39,8 +39,8 @@ or contact your system administrator. === Security privileges You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key` -cluster privileges to use API keys in {kib}. To manage roles, open the menu, then go to -*Stack Management > Security > Roles*, or use the <>. +cluster privileges to use API keys in {kib}. To manage roles, open the main menu, then click +*Stack Management > Roles*, or use the <>. [float] diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 3af49753db664..150004b3ad691 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -12,7 +12,7 @@ NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in y [[xpack-kibana-role-management]] === {kib} role management -To create a role that grants {kib} privileges, open the menu, go to *Stack Management > Security > Roles* and click **Create role**. +To create a role that grants {kib} privileges, open the main menu, click *Stack Management > Roles*, then click *Create role*. [[adding_kibana_privileges]] ==== Adding {kib} privileges diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index e1a46a415fe68..b5ab57d8f525a 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -13,7 +13,7 @@ auditing. For more information, see [float] === Users -To create and manage users, open the menu, then go to *Stack Management > Security > Users*. +To create and manage users, open the main menu, then click *Stack Management > Users*. You can also change their passwords and roles. For more information about authentication and built-in users, see {ref}/setting-up-authentication.html[Setting up user authentication]. @@ -21,7 +21,7 @@ authentication and built-in users, see [float] === Roles -To manage roles, open the menu, then go to *Stack Management > Security > Roles*, or use +To manage roles, open the main menu, then click *Stack Management > Roles*, or use the <>. For more information on configuring roles for {kib}, see <>. For a more holistic overview of configuring roles for the entire stack, diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index bf7be6284b1a9..2088110f6de21 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -45,7 +45,7 @@ through in this tutorial: [float] ==== Create a role -Open the menu, then go to *Stack Management > Security > Roles* +Open the main menu, then click *Stack Management > Roles* for an overview of your roles. This view provides actions for you to create, edit, and delete roles. @@ -90,7 +90,7 @@ image::security/images/role-space-visualization.png["Associate space"] [float] ==== Create the developer user account with the proper roles -. Open the menu, then go to *Stack Management > Security > Users*. +. Open the main menu, then click *Stack Management > Users*. . Click **Create user**, then give the user the `dev-mortgage` and `monitoring-user` roles, which are required for *Stack Monitoring* users. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index daf9720a0f1d8..6e7fc0c212f07 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -24,11 +24,11 @@ to report on and the {es} indices. [[reporting-roles-management-ui]] === If you are using the `native` realm -To assign roles, open the menu, then go to *Stack Management > Security > Roles*, use the <>. +To assign roles, use the *Roles* UI or <>. This example shows how to use *Roles* page to create a user who has a custom role and the `reporting_user` role. -. Open the menu, then go to *Stack Management > Security > Roles*. +. Open the main menu, then click *Stack Management > Roles*. . Click *Create role*, then give the role a name, for example, `custom_reporting_user`. @@ -51,7 +51,7 @@ that provides read and write privileges in . Save your new role. -. Open the menu, then go to *Stack Management > Security > Users*, add a new user, and assign the user the built-in +. Open the main menu, then click *Stack Management > Users*, add a new user, and assign the user the built-in `reporting_user` role and your new custom role, `custom_reporting_user`. [float] @@ -69,10 +69,10 @@ If you use a different pattern for the `xpack.reporting.index` setting, you must create a custom role with appropriate access to the index, similar to the following: -. Open the menu, then go to *Stack Management >Security > Roles*. +. Open the main menu, then click *Stack Management > Roles*. . Click *Create role*, then name the role `custom-reporting-user`. . Specify the custom index and assign it the `all` index privilege. -. Open the menu, then go to *Stack Management > Security > Users* and create a new user with +. Open the main menu, then click *Stack Management > Users* and create a new user with the `kibana_system` role and the `custom-reporting-user` role. . Configure {kib} to use the new account: [source,js] diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc index 661c319af827f..3f9a17e98d77f 100644 --- a/docs/user/security/role-mappings/index.asciidoc +++ b/docs/user/security/role-mappings/index.asciidoc @@ -9,7 +9,7 @@ or SAML. Role mappings have no effect for users inside the `native` or `file` realms. -To manage your role mappings, open the menu, then go to *Stack Management > Security > Role Mappings*. +To manage your role mappings, open the main menu, then click *Stack Management > Role Mappings*. With *Role mappings*, you can: @@ -23,7 +23,7 @@ image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"] [float] === Create a role mapping -. Open the menu, then go to *Stack Management > Security > Role Mappings*. +. Open the main menu, then click *Stack Management > Role Mappings*. . Click *Create role mapping*. . Give your role mapping a unique name, and choose which roles you wish to assign to your users. + diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index e7bd297a3ebb5..613ec88ed0edc 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -81,10 +81,10 @@ use {kib}. For more information on Basic Authentication and additional methods of authenticating {kib} users, see <>. -To manage privileges, open the menu, then go to *Stack Management > Security > Roles*. +To manage privileges, open the main menu, then click *Stack Management > Roles*. -If you're using the native realm with Basic Authentication, open then menu, -then go to *Stack Management > Security > Users* to assign roles, or use the +If you're using the native realm with Basic Authentication, open then main menu, +then click *Stack Management > Users* to assign roles, or use the {ref}/security-api.html#security-user-apis[user management APIs]. For example, the following creates a user named `jacknich` and assigns it the `kibana_admin` role: diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc index 31e7d157d1bc7..54bdfff8e0bbb 100644 --- a/docs/user/setup.asciidoc +++ b/docs/user/setup.asciidoc @@ -1,5 +1,5 @@ [[setup]] -= Set up Kibana += Set up [partintro] -- diff --git a/docs/visualize/images/timelion-copy-expression.png b/docs/visualize/images/timelion-copy-expression.png deleted file mode 100644 index 376bf7919166e..0000000000000 Binary files a/docs/visualize/images/timelion-copy-expression.png and /dev/null differ diff --git a/package.json b/package.json index b965f50cd468c..3a2d13fd5ef3b 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ }, "dependencies": { "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@elastic/eui": "29.5.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", @@ -124,6 +124,7 @@ "@elastic/safer-lodash-set": "0.0.0", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", + "@kbn/ace": "1.0.0", "@kbn/analytics": "1.0.0", "@kbn/apm-config-loader": "1.0.0", "@kbn/config": "1.0.0", @@ -131,11 +132,11 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", + "@kbn/monaco": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-framework": "1.0.0", - "@kbn/ace": "1.0.0", - "@kbn/monaco": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/pdfmake": "^0.1.15", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", @@ -227,7 +228,7 @@ "@babel/register": "^7.10.5", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "23.2.1", + "@elastic/charts": "24.0.0", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 6ed3ae2eb2fb7..0f9b917e7f05a 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -12,7 +12,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@kbn/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/packages/kbn-monaco/src/esql/constants.ts b/packages/kbn-monaco/src/esql/constants.ts new file mode 100644 index 0000000000000..59bf9a94d05b2 --- /dev/null +++ b/packages/kbn-monaco/src/esql/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ID = 'esql'; diff --git a/packages/kbn-monaco/src/esql/index.ts b/packages/kbn-monaco/src/esql/index.ts new file mode 100644 index 0000000000000..b0e25af760a26 --- /dev/null +++ b/packages/kbn-monaco/src/esql/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ID } from './constants'; +import { lexerRules } from './lexer_rules'; + +export const EsqlLang = { ID, lexerRules }; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts similarity index 98% rename from packages/kbn-monaco/src/xjson/lexer_rules/esql.ts rename to packages/kbn-monaco/src/esql/lexer_rules/esql.ts index e75b1013d3727..8badc8ffc4184 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/esql.ts +++ b/packages/kbn-monaco/src/esql/lexer_rules/esql.ts @@ -17,9 +17,7 @@ * under the License. */ -import { monaco } from '../../monaco'; - -export const ID = 'esql'; +import { monaco } from '../../monaco_imports'; const brackets = [ { open: '[', close: ']', token: 'delimiter.square' }, diff --git a/packages/kbn-monaco/src/esql/lexer_rules/index.ts b/packages/kbn-monaco/src/esql/lexer_rules/index.ts new file mode 100644 index 0000000000000..5210bc2411716 --- /dev/null +++ b/packages/kbn-monaco/src/esql/lexer_rules/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { lexerRules } from './esql'; diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts index 9213a1bfe1327..2a8467d6ef8fd 100644 --- a/packages/kbn-monaco/src/index.ts +++ b/packages/kbn-monaco/src/index.ts @@ -17,8 +17,12 @@ * under the License. */ -export { monaco } from './monaco'; +// global setup for supported languages +import './register_globals'; + +export { monaco } from './monaco_imports'; export { XJsonLang } from './xjson'; +export { PainlessLang } from './painless'; /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api'; diff --git a/packages/kbn-monaco/src/monaco.ts b/packages/kbn-monaco/src/monaco_imports.ts similarity index 100% rename from packages/kbn-monaco/src/monaco.ts rename to packages/kbn-monaco/src/monaco_imports.ts diff --git a/packages/kbn-monaco/src/painless/constants.ts b/packages/kbn-monaco/src/painless/constants.ts new file mode 100644 index 0000000000000..32bbc0aaaa0be --- /dev/null +++ b/packages/kbn-monaco/src/painless/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ID = 'painless'; diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts new file mode 100644 index 0000000000000..2ff1f4a19f9bd --- /dev/null +++ b/packages/kbn-monaco/src/painless/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ID } from './constants'; +import { lexerRules } from './lexer_rules'; + +export const PainlessLang = { ID, lexerRules }; diff --git a/packages/kbn-monaco/src/painless/lexer_rules/index.ts b/packages/kbn-monaco/src/painless/lexer_rules/index.ts new file mode 100644 index 0000000000000..7cf9064c6aa51 --- /dev/null +++ b/packages/kbn-monaco/src/painless/lexer_rules/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { lexerRules } from './painless'; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts similarity index 87% rename from packages/kbn-monaco/src/xjson/lexer_rules/painless.ts rename to packages/kbn-monaco/src/painless/lexer_rules/painless.ts index 676eb3134026a..2f4383911c9ad 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/painless.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts @@ -17,16 +17,9 @@ * under the License. */ -import { monaco } from '../../monaco'; +import { monaco } from '../../monaco_imports'; -export const ID = 'painless'; - -/** - * Extends the default type for a Monarch language so we can use - * attribute references (like @keywords to reference the keywords list) - * in the defined tokenizer - */ -interface Language extends monaco.languages.IMonarchLanguage { +export interface Language extends monaco.languages.IMonarchLanguage { default: string; brackets: any; keywords: string[]; @@ -41,8 +34,7 @@ interface Language extends monaco.languages.IMonarchLanguage { } export const lexerRules = { - default: 'invalid', - tokenPostfix: '', + default: '', // painless does not use < >, so we define our own brackets: [ ['{', '}', 'delimiter.curly'], @@ -136,9 +128,9 @@ export const lexerRules = { }, ], // whitespace - [/[ \t\r\n]+/, { token: 'whitespace' }], + [/[ \t\r\n]+/, '@whitespace'], // comments - [/\/\*/, 'comment', '@comment'], + // [/\/\*/, 'comment', '@comment'], [/\/\/.*$/, 'comment'], // brackets [/[{}()\[\]]/, '@brackets'], @@ -168,7 +160,6 @@ export const lexerRules = { // strings single quoted [/'([^'\\]|\\.)*$/, 'string.invalid'], // string without termination [/'/, 'string', '@string_sq'], - [/"""/, { token: 'punctuation.end_triple_quote', nextEmbedded: '@pop' }], ], comment: [ [/[^\/*]+/, 'comment'], @@ -189,6 +180,3 @@ export const lexerRules = { ], }, } as Language; - -monaco.languages.register({ id: ID }); -monaco.languages.setMonarchTokensProvider(ID, lexerRules); diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts new file mode 100644 index 0000000000000..b9e94803b7542 --- /dev/null +++ b/packages/kbn-monaco/src/register_globals.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 { XJsonLang } from './xjson'; +import { PainlessLang } from './painless'; +import { EsqlLang } from './esql'; +import { monaco } from './monaco_imports'; +// @ts-ignore +import xJsonWorkerSrc from '!!raw-loader!../target/public/xjson.editor.worker.js'; +// @ts-ignore +import defaultWorkerSrc from '!!raw-loader!../target/public/default.editor.worker.js'; + +/** + * Register languages and lexer rules + */ +monaco.languages.register({ id: XJsonLang.ID }); +monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules); +monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration); +monaco.languages.register({ id: PainlessLang.ID }); +monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules); +monaco.languages.register({ id: EsqlLang.ID }); +monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules); + +/** + * Create web workers by language ID + */ +const mapLanguageIdToWorker: { [key: string]: any } = { + [XJsonLang.ID]: xJsonWorkerSrc, +}; + +// @ts-ignore +window.MonacoEnvironment = { + getWorker: (module: string, languageId: string) => { + const workerSrc = mapLanguageIdToWorker[languageId] || defaultWorkerSrc; + + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); + }, +}; diff --git a/packages/kbn-monaco/src/xjson/index.ts b/packages/kbn-monaco/src/xjson/index.ts index 8a4644a3792d2..c372f02c09c76 100644 --- a/packages/kbn-monaco/src/xjson/index.ts +++ b/packages/kbn-monaco/src/xjson/index.ts @@ -22,5 +22,6 @@ */ import './language'; import { ID } from './constants'; +import { lexerRules, languageConfiguration } from './lexer_rules'; -export const XJsonLang = { ID }; +export const XJsonLang = { ID, lexerRules, languageConfiguration }; diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts index 4ae7f2402ed2f..9759dc1b24401 100644 --- a/packages/kbn-monaco/src/xjson/language.ts +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -19,32 +19,12 @@ // This file contains a lot of single setup logic for registering a language globally -import { monaco } from '../monaco'; +import { monaco } from '../monaco_imports'; import { WorkerProxyService } from './worker_proxy_service'; -import { registerLexerRules } from './lexer_rules'; import { ID } from './constants'; -// @ts-ignore -import workerSrc from '!!raw-loader!../../target/public/xjson.editor.worker.js'; const wps = new WorkerProxyService(); -// Register rules against shared monaco instance. -registerLexerRules(monaco); - -// In future we will need to make this map languages to workers using "id" and/or "label" values -// that get passed in. Also this should not live inside the "xjson" dir directly. We can update this -// once we have another worker. -// @ts-ignore -window.MonacoEnvironment = { - getWorker: (module: string, languageId: string) => { - if (languageId === ID) { - // In kibana we will probably build this once and then load with raw-loader - const blob = new Blob([workerSrc], { type: 'application/javascript' }); - return new Worker(URL.createObjectURL(blob)); - } - }, -}; - monaco.languages.onLanguage(ID, async () => { return wps.setup(); }); diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts index 515de09510a61..7393c6a68c1bf 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/index.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/index.ts @@ -17,17 +17,4 @@ * under the License. */ -/* eslint-disable-next-line @kbn/eslint/module_migration */ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import * as xJson from './xjson'; -import * as esql from './esql'; -import * as painless from './painless'; - -export const registerLexerRules = (m: typeof monaco) => { - m.languages.register({ id: xJson.ID }); - m.languages.setMonarchTokensProvider(xJson.ID, xJson.lexerRules); - m.languages.register({ id: painless.ID }); - m.languages.setMonarchTokensProvider(painless.ID, painless.lexerRules); - m.languages.register({ id: esql.ID }); - m.languages.setMonarchTokensProvider(esql.ID, esql.lexerRules); -}; +export { lexerRules, languageConfiguration } from './xjson'; diff --git a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts index d6fea9e91acfb..e0c566fd3b0f2 100644 --- a/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts +++ b/packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts @@ -17,15 +17,10 @@ * under the License. */ -import { monaco } from '../../monaco'; -import { ID } from '../constants'; -import './painless'; -import './esql'; +import { monaco } from '../../monaco_imports'; import { globals } from './shared'; -export { ID }; - export const lexerRules: monaco.languages.IMonarchLanguage = { ...(globals as any), @@ -124,11 +119,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = { }, }; -monaco.languages.register({ - id: ID, -}); -monaco.languages.setMonarchTokensProvider(ID, lexerRules); -monaco.languages.setLanguageConfiguration(ID, { +export const languageConfiguration: monaco.languages.LanguageConfiguration = { brackets: [ ['{', '}'], ['[', ']'], @@ -138,4 +129,4 @@ monaco.languages.setLanguageConfiguration(ID, { { open: '[', close: ']' }, { open: '"', close: '"' }, ], -}); +}; diff --git a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts index 548a413a483d9..c0e735b294484 100644 --- a/packages/kbn-monaco/src/xjson/worker_proxy_service.ts +++ b/packages/kbn-monaco/src/xjson/worker_proxy_service.ts @@ -18,7 +18,7 @@ */ import { ParseResult } from './grammar'; -import { monaco } from '../monaco'; +import { monaco } from '../monaco_imports'; import { XJsonWorker } from './worker'; import { ID } from './constants'; diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js index 1a7d8c031670c..53f440689a233 100644 --- a/packages/kbn-monaco/webpack.config.js +++ b/packages/kbn-monaco/webpack.config.js @@ -19,33 +19,40 @@ const path = require('path'); -const createLangWorkerConfig = (lang) => ({ - mode: 'production', - entry: path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`), - output: { - path: path.resolve(__dirname, 'target/public'), - filename: `${lang}.editor.worker.js`, - }, - resolve: { - modules: ['node_modules'], - extensions: ['.js', '.ts', '.tsx'], - }, - stats: 'errors-only', - module: { - rules: [ - { - test: /\.(js|ts)$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], +const createLangWorkerConfig = (lang) => { + const entry = + lang === 'default' + ? 'monaco-editor/esm/vs/editor/editor.worker.js' + : path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`); + + return { + mode: 'production', + entry, + output: { + path: path.resolve(__dirname, 'target/public'), + filename: `${lang}.editor.worker.js`, + }, + resolve: { + modules: ['node_modules'], + extensions: ['.js', '.ts', '.tsx'], + }, + stats: 'errors-only', + module: { + rules: [ + { + test: /\.(js|ts)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], + }, }, }, - }, - ], - }, -}); + ], + }, + }; +}; -module.exports = [createLangWorkerConfig('xjson')]; +module.exports = [createLangWorkerConfig('xjson'), createLangWorkerConfig('default')]; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index fd0be15affab3..c660d37222504 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -14,7 +14,7 @@ pageLoadAssetSize: dashboard: 374194 dashboardEnhanced: 65646 dashboardMode: 22716 - data: 1170713 + data: 1287839 dataEnhanced: 50420 devTools: 38637 discover: 105145 diff --git a/packages/kbn-plugin-generator/README.md b/packages/kbn-plugin-generator/README.md index 9ff9a8aa95ca2..bee8e6c2ca783 100644 --- a/packages/kbn-plugin-generator/README.md +++ b/packages/kbn-plugin-generator/README.md @@ -51,7 +51,7 @@ yarn kbn bootstrap Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it: -> ***NOTE:*** All of these scripts should be run from the generated plugin. +> ***NOTE:*** The following scripts should be run from the generated plugin. - `yarn kbn bootstrap` @@ -59,14 +59,6 @@ Generated plugins receive a handful of scripts that can be used during developme > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - `yarn build` Build a distributable archive of your plugin. @@ -75,4 +67,15 @@ Generated plugins receive a handful of scripts that can be used during developme Run the server tests using mocha. + +To start kibana run the following command from Kibana root. + + - `yarn start` + + Start kibana and it will automatically include this plugin. You can pass any arguments that you would normally send to `bin/kibana` + + ``` + yarn start --elasticsearch.hosts http://localhost:9220 + ``` + For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks run `yarn run` or take a look in the `package.json` file. diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index a2c4e1e2134e7..1f86122d7e129 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -4,6 +4,9 @@ "private": true, "description": "Just some helpers for kibana plugin devs.", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "main": "target/index.js", "bin": { "plugin-helpers": "bin/plugin-helpers.js" diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 557f38ec740fc..9684201c72384 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -12,6 +12,9 @@ }, "author": "", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "bugs": { "url": "https://github.com/jbudz/spec-to-console/issues" }, diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index dc2647b7b5757..c3bc65059d4a6 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,12 +20,7 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: [ - '@kbn/storybook/preset', - '@storybook/addon-a11y', - '@storybook/addon-knobs', - '@storybook/addon-essentials', - ], + addons: ['@kbn/storybook/preset', '@storybook/addon-a11y', '@storybook/addon-essentials'], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts index 1390c44d84a07..a95215a0044f1 100644 --- a/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts +++ b/packages/kbn-test/src/jest/integration_tests/junit_reporter.test.ts @@ -24,13 +24,13 @@ import { readFileSync } from 'fs'; import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { REPO_ROOT } from '@kbn/utils'; const MINUTE = 1000 * 60; const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); +const XML_PATH = getUniqueJunitReportPath(FIXTURE_DIR, 'JUnit Reporter Integration Test'); afterAll(async () => { await del(TARGET_DIR); diff --git a/packages/kbn-test/src/jest/junit_reporter.ts b/packages/kbn-test/src/jest/junit_reporter.ts index 0712584122e05..b6e964c22adfc 100644 --- a/packages/kbn-test/src/jest/junit_reporter.ts +++ b/packages/kbn-test/src/jest/junit_reporter.ts @@ -27,7 +27,7 @@ import type { Config } from '@jest/types'; import { AggregatedResult, Test, BaseReporter } from '@jest/reporters'; import { escapeCdata } from '../mocha/xml'; -import { makeJunitReportPath } from './report_path'; +import { getUniqueJunitReportPath } from './report_path'; interface ReporterOptions { reportName?: string; @@ -115,7 +115,7 @@ export default class JestJUnitReporter extends BaseReporter { }); }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = root.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-test/src/jest/report_path.ts b/packages/kbn-test/src/jest/report_path.ts index fe122c349c193..c9cf3ce454e6a 100644 --- a/packages/kbn-test/src/jest/report_path.ts +++ b/packages/kbn-test/src/jest/report_path.ts @@ -17,14 +17,24 @@ * under the License. */ -import { resolve } from 'path'; +import Fs from 'fs'; +import Path from 'path'; + import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; -export function makeJunitReportPath(rootDirectory: string, reportName: string) { - return resolve( +export function getUniqueJunitReportPath( + rootDirectory: string, + reportName: string, + counter?: number +): string { + const path = Path.resolve( rootDirectory, 'target/junit', process.env.JOB || '.', - `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}.xml` + `TEST-${CI_PARALLEL_PROCESS_PREFIX}${reportName}${counter ? `-${counter}` : ''}.xml` ); + + return Fs.existsSync(path) + ? getUniqueJunitReportPath(rootDirectory, reportName, (counter ?? 0) + 1) + : path; } diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 00a11432dd9e8..dc7d161eca5a3 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -25,13 +25,14 @@ import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; import expect from '@kbn/expect'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { setupJUnitReportGeneration } from '../junit_report_generation'; const PROJECT_DIR = resolve(__dirname, 'fixtures/project'); const DURATION_REGEX = /^\d+\.\d{3}$/; const ISO_DATE_SEC_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/; +const XML_PATH = getUniqueJunitReportPath(PROJECT_DIR, 'test'); describe('dev/mocha/junit report generation', () => { afterEach(() => { @@ -50,9 +51,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise((resolve) => mocha.run(resolve)); - const report = await fcb((cb) => - parseString(readFileSync(makeJunitReportPath(PROJECT_DIR, 'test')), cb) - ); + const report = await fcb((cb) => parseString(readFileSync(XML_PATH), cb)); // test case results are wrapped in expect(report).to.eql({ diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 7e39c32ee4db8..9ac9bd18548f4 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -22,7 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; import xmlBuilder from 'xmlbuilder'; -import { makeJunitReportPath } from '@kbn/test'; +import { getUniqueJunitReportPath } from '@kbn/test'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../'; @@ -140,7 +140,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { } }); - const reportPath = makeJunitReportPath(rootDirectory, reportName); + const reportPath = getUniqueJunitReportPath(rootDirectory, reportName); const reportXML = builder.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 980d9d02317b6..b1b5d6e2b419e 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "23.2.1", + "@elastic/charts": "24.0.0", "@elastic/eui": "29.5.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 71900ab982f3d..b70680594151b 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -103,6 +103,10 @@ interface ListenerOptions { export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { const server = new Server(serverOptions); + // remove fix + test as soon as update node.js to v12.19 https://github.com/elastic/kibana/pull/61587 + server.listener.headersTimeout = + listenerOptions.keepaliveTimeout + 2 * server.listener.headersTimeout; + server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; server.listener.setTimeout(listenerOptions.socketTimeout); server.listener.on('timeout', (socket) => { diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 0170e94867c06..3d0eba6de632e 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -313,7 +313,6 @@ describe('KibanaRequest', () => { expect(resp3.body).toEqual({ requestId: 'gamma' }); }); }); - describe('request uuid', () => { it('generates a UUID', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index b4d91926f13f4..412396644648e 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -50,6 +50,8 @@ configService.atPath.mockReturnValue( allowFromAnyIp: true, ipAllowlist: [], }, + keepaliveTimeout: 120_000, + socketTimeout: 120_000, } as any) ); diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index f5cf6c85fcbef..274d7a4e5a488 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -138,6 +138,7 @@ kibana_vars=( tilemap.url timelion.enabled vega.enableExternalUrls + xpack.actions.proxyUrl xpack.apm.enabled xpack.apm.serviceMapEnabled xpack.apm.ui.enabled diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index fba3dcee0a7cb..bd30efcb1c6d3 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -53,9 +53,15 @@ RUN for iter in {1..10}; do \ (exit $exit_code) # Add an init process, check the checksum to make sure it's a match -RUN curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 -RUN echo "37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 /usr/local/bin/dumb-init" | sha256sum -c - -RUN chmod +x /usr/local/bin/dumb-init +RUN set -e ; \ + TINI_VERSION='v0.19.0' ; \ + TINI_BIN='tini-amd64' ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}" ; \ + curl --retry 8 -S -L -O "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${TINI_BIN}.sha256sum" ; \ + sha256sum -c "${TINI_BIN}.sha256sum" ; \ + rm "${TINI_BIN}.sha256sum" ; \ + mv "${TINI_BIN}" /bin/tini ; \ + chmod +x /bin/tini RUN mkdir /usr/share/fonts/local RUN curl -L -o /usr/share/fonts/local/NotoSansCJK-Regular.ttc https://github.com/googlefonts/noto-cjk/raw/NotoSansV2.001/NotoSansCJK-Regular.ttc @@ -125,6 +131,6 @@ RUN mkdir /licenses && \ USER kibana -ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] +ENTRYPOINT ["/bin/tini", "--"] CMD ["/usr/local/bin/kibana-docker"] diff --git a/src/fixtures/telemetry_collectors/externally_defined_collector.ts b/src/fixtures/telemetry_collectors/externally_defined_collector.ts index 00a8d643e27b3..decee9df28185 100644 --- a/src/fixtures/telemetry_collectors/externally_defined_collector.ts +++ b/src/fixtures/telemetry_collectors/externally_defined_collector.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { CollectorSet, CollectorOptions } from '../../plugins/usage_collection/server/collector'; +import { + CollectorSet, + UsageCollectorOptions, +} from '../../plugins/usage_collection/server/collector'; import { loggerMock } from '../../core/server/logging/logger.mock'; const collectorSet = new CollectorSet({ @@ -28,7 +31,7 @@ interface Usage { locale: string; } -function createCollector(): CollectorOptions { +function createCollector(): UsageCollectorOptions { return { type: 'from_fn_collector', isReady: () => true, @@ -46,7 +49,7 @@ function createCollector(): CollectorOptions { } export function defineCollectorFromVariable() { - const fromVarCollector: CollectorOptions = { + const fromVarCollector: UsageCollectorOptions = { type: 'from_variable_collector', isReady: () => true, fetch(): Usage { diff --git a/src/fixtures/telemetry_collectors/unmapped_collector.ts b/src/fixtures/telemetry_collectors/unmapped_collector.ts index 1ea360fcd9e96..143bb168aff35 100644 --- a/src/fixtures/telemetry_collectors/unmapped_collector.ts +++ b/src/fixtures/telemetry_collectors/unmapped_collector.ts @@ -28,6 +28,7 @@ interface Usage { locale: string; } +// @ts-expect-error Intentionally not specifying `schema` export const myCollector = makeUsageCollector({ type: 'unmapped_collector', isReady: () => true, diff --git a/src/legacy/server/i18n/constants.ts b/src/legacy/server/i18n/constants.ts index 96fa420d4c6e1..a7a410dbcb5b3 100644 --- a/src/legacy/server/i18n/constants.ts +++ b/src/legacy/server/i18n/constants.ts @@ -18,8 +18,3 @@ */ export const I18N_RC = '.i18nrc.json'; - -/** - * The type name used within the Monitoring index to publish localization stats. - */ -export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts b/src/legacy/server/i18n/localization/telemetry_localization_collector.ts index 89566dfd4ef68..fb837f5ae28df 100644 --- a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts +++ b/src/legacy/server/i18n/localization/telemetry_localization_collector.ts @@ -21,7 +21,6 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getIntegrityHashes, Integrities } from './file_integrity'; -import { KIBANA_LOCALIZATION_STATS_TYPE } from '../constants'; export interface UsageStats { locale: string; @@ -63,14 +62,20 @@ export function createCollectorFetch({ }; } +// TODO: Migrate out of the Legacy dir export function registerLocalizationUsageCollector( usageCollection: UsageCollectionSetup, helpers: LocalizationUsageCollectorHelpers ) { - const collector = usageCollection.makeUsageCollector({ - type: KIBANA_LOCALIZATION_STATS_TYPE, + const collector = usageCollection.makeUsageCollector({ + type: 'localization', isReady: () => true, fetch: createCollectorFetch(helpers), + schema: { + locale: { type: 'keyword' }, + integrities: { DYNAMIC_KEY: { type: 'text' } }, + labelsCount: { type: 'long' }, + }, }); usageCollection.registerCollector(collector); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 794168132abb2..e9fa2833c3db5 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; +import 'brace/mode/json'; import { EuiBadge, diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 3c4fac81c2c7c..be7836de31246 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -91,6 +91,17 @@ describe('Field', function () { expect(fieldC.searchable).toEqual(false); }); + it('calculates visualizable', () => { + const field = getField({ type: 'unknown' }); + expect(field.visualizable).toEqual(false); + + const fieldB = getField({ type: 'conflict' }); + expect(fieldB.visualizable).toEqual(false); + + const fieldC = getField({ aggregatable: false, scripted: false }); + expect(fieldC.visualizable).toEqual(false); + }); + it('calculates aggregatable', () => { const field = getField({ aggregatable: true, scripted: false }); expect(field.aggregatable).toEqual(true); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 808afc3449c2a..4a22508f7fef3 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -18,6 +18,7 @@ */ import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -129,7 +130,8 @@ export class IndexPatternField implements IFieldType { } public get visualizable() { - return this.aggregatable; + const notVisualizableFieldTypes: string[] = [KBN_FIELD_TYPES.UNKNOWN, KBN_FIELD_TYPES.CONFLICT]; + return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type); } public toJSON() { 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 9fd43be8dc5b3..9085ae07bbe3e 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 @@ -44,7 +44,6 @@ function create(id: string) { return new IndexPattern({ spec: { id, type, version, timeFieldName, fields, title }, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -214,7 +213,6 @@ describe('IndexPattern', () => { const spec = indexPattern.toSpec(); const restoredPattern = new IndexPattern({ spec, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index d38df68e9f428..a0f27078543a9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,7 +18,6 @@ */ import _, { each, reject } from 'lodash'; -import { SavedObjectsClientCommon } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -31,10 +30,9 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; interface IndexPatternDeps { spec?: IndexPatternSpec; - savedObjectsClient: SavedObjectsClientCommon; fieldFormats: FieldFormatsStartCommon; - shortDotsEnable: boolean; - metaFields: string[]; + shortDotsEnable?: boolean; + metaFields?: string[]; } interface SavedObjectBody { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index bfd0dc9d946c2..fd3d7a1d138fd 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -435,7 +435,6 @@ export class IndexPatternsService { const indexPattern = new IndexPattern({ spec, - savedObjectsClient: this.savedObjectsClient, fieldFormats: this.fieldFormats, shortDotsEnable, metaFields, diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap new file mode 100644 index 0000000000000..d5ddaa31b8ac3 --- /dev/null +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tabifyDocs converts fields by default 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs converts source if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + ], + "rows": Array [ + Object { + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs skips nested fields if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested", + "meta": Object { + "field": "nested", + "index": "test-index", + "params": undefined, + "type": "object", + }, + "name": "nested", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs works without provided index pattern 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 90ac3f2fb730b..9e6657f5e8d83 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -17,6 +17,26 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; +import { SearchSource } from '../search_source'; +import { tabifyAggResponse } from './tabify'; +import { tabifyDocs, TabifyDocsOptions } from './tabify_docs'; +import { TabbedResponseWriterOptions } from './types'; + +export const tabify = ( + searchSource: SearchSource, + esResponse: SearchResponse, + opts: Partial | TabifyDocsOptions +) => { + return !esResponse.aggregations + ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions) + : tabifyAggResponse( + searchSource.getField('aggs'), + esResponse, + opts as Partial + ); +}; + export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts new file mode 100644 index 0000000000000..a1218928561c6 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { tabifyDocs } from './tabify_docs'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { SearchResponse } from 'elasticsearch'; + +describe('tabifyDocs', () => { + const fieldFormats = { + getInstance: (id: string) => ({ toJSON: () => ({ id }) }), + getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), + }; + + const index = new IndexPattern({ + spec: { + id: 'test-index', + fields: { + sourceTest: { name: 'sourceTest', type: 'number', searchable: true, aggregatable: true }, + fieldTest: { name: 'fieldTest', type: 'number', searchable: true, aggregatable: true }, + 'nested.field': { + name: 'nested.field', + type: 'number', + searchable: true, + aggregatable: true, + }, + }, + }, + fieldFormats: fieldFormats as any, + }); + + const response = { + hits: { + hits: [ + { + _source: { sourceTest: 123 }, + fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + }, + ], + }, + } as SearchResponse; + + it('converts fields by default', () => { + const table = tabifyDocs(response, index); + expect(table).toMatchSnapshot(); + }); + + it('converts source if option is set', () => { + const table = tabifyDocs(response, index, { source: true }); + expect(table).toMatchSnapshot(); + }); + + it('skips nested fields if option is set', () => { + const table = tabifyDocs(response, index, { shallow: true }); + expect(table).toMatchSnapshot(); + }); + + it('works without provided index pattern', () => { + const table = tabifyDocs(response); + expect(table).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts new file mode 100644 index 0000000000000..78ebee9e65717 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { isPlainObject } from 'lodash'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; + +export function flattenHit( + hit: Record, + indexPattern?: IndexPattern, + shallow: boolean = false +) { + const flat = {} as Record; + + function flatten(obj: Record, keyPrefix: string = '') { + for (const [k, val] of Object.entries(obj)) { + const key = keyPrefix + k; + + const field = indexPattern?.fields.getByName(key); + + if (!shallow) { + const isNestedField = field?.type === 'nested'; + if (Array.isArray(val) && !isNestedField) { + val.forEach((v) => isPlainObject(v) && flatten(v, key + '.')); + continue; + } + } else if (flat[key] !== undefined) { + continue; + } + + const hasValidMapping = field?.type !== 'conflict'; + const isValue = !isPlainObject(val); + + if (hasValidMapping || isValue) { + if (!flat[key]) { + flat[key] = val; + } else if (Array.isArray(flat[key])) { + flat[key].push(val); + } else { + flat[key] = [flat[key], val]; + } + continue; + } + + flatten(val, key + '.'); + } + } + + flatten(hit); + return flat; +} + +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; +} + +export const tabifyDocs = ( + esResponse: SearchResponse, + index?: IndexPattern, + params: TabifyDocsOptions = {} +): Datatable => { + const columns: DatatableColumn[] = []; + + const rows = esResponse.hits.hits + .map((hit) => { + const toConvert = params.source ? hit._source : hit.fields; + const flat = flattenHit(toConvert, index, params.shallow); + for (const [key, value] of Object.entries(flat)) { + const field = index?.fields.getByName(key); + const fieldName = field?.name || key; + if (!columns.find((c) => c.id === fieldName)) { + const fieldType = (field?.type as DatatableColumnType) || typeof value; + const formatter = field && index?.getFormatterForField(field); + columns.push({ + id: fieldName, + name: fieldName, + meta: { + type: fieldType, + field: fieldName, + index: index?.id, + params: formatter ? formatter.toJSON() : undefined, + }, + }); + } + } + return flat; + }) + .filter((hit) => hit); + + return { + type: 'datatable', + columns, + rows, + }; +}; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 1390b28ec830d..81fa6d4ba20db 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1598,13 +1598,13 @@ export interface OptionedValueProp { value: string; } -// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "PainlessError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class PainlessError extends KbnError { - // Warning: (ae-forgotten-export) The symbol "EsError" needs to be exported by the entry point index.d.ts - constructor(err: EsError, request: IKibanaSearchRequest); +export class PainlessError extends EsError { + // Warning: (ae-forgotten-export) The symbol "IEsError" needs to be exported by the entry point index.d.ts + constructor(err: IEsError, request: IKibanaSearchRequest); // (undocumented) getErrorMessage(application: ApplicationStart): JSX.Element; // (undocumented) @@ -2134,6 +2134,7 @@ export interface SearchSourceFields { version?: boolean; } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "SearchTimeoutError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -2283,7 +2284,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx new file mode 100644 index 0000000000000..53d00159b836b --- /dev/null +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; +import { KbnError } from '../../../../kibana_utils/common'; +import { IEsError } from './types'; +import { getRootCause } from './utils'; + +export class EsError extends KbnError { + constructor(protected readonly err: IEsError) { + super('EsError'); + } + + public getErrorMessage(application: ApplicationStart) { + const rootCause = getRootCause(this.err)?.reason; + + return ( + <> + + {rootCause ? ( + + {rootCause} + + ) : null} + + ); + } +} diff --git a/src/plugins/data/public/search/errors/http_error.tsx b/src/plugins/data/public/search/errors/http_error.tsx new file mode 100644 index 0000000000000..58ae3148804a2 --- /dev/null +++ b/src/plugins/data/public/search/errors/http_error.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function getHttpError(message: string) { + return ( + <> + {i18n.translate('data.errors.fetchError', { + defaultMessage: + 'Check your network and proxy configuration. If the problem persists, contact your network administrator.', + })} + + + + {message} + + + ); +} diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts index 6082e758a8bad..01357d25334a3 100644 --- a/src/plugins/data/public/search/errors/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -17,5 +17,9 @@ * under the License. */ +export * from './es_error'; export * from './painless_error'; export * from './timeout_error'; +export * from './utils'; +export * from './types'; +export * from './http_error'; diff --git a/src/plugins/data/public/search/errors/painless_error.tsx b/src/plugins/data/public/search/errors/painless_error.tsx index 244f205469a2f..282a602d358c7 100644 --- a/src/plugins/data/public/search/errors/painless_error.tsx +++ b/src/plugins/data/public/search/errors/painless_error.tsx @@ -22,22 +22,15 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiSpacer, EuiText, EuiCodeBlock } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ApplicationStart } from 'kibana/public'; -import { KbnError } from '../../../../kibana_utils/common'; -import { EsError, isEsError } from './types'; +import { IEsError, isEsError } from './types'; +import { EsError } from './es_error'; +import { getRootCause } from './utils'; import { IKibanaSearchRequest } from '..'; -export class PainlessError extends KbnError { +export class PainlessError extends EsError { painlessStack?: string; - constructor(err: EsError, request: IKibanaSearchRequest) { - const rootCause = getRootCause(err as EsError); - - super( - i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { - defaultMessage: "Error executing Painless script: '{script}'.", - values: { script: rootCause?.script }, - }) - ); - this.painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined; + constructor(err: IEsError, request: IKibanaSearchRequest) { + super(err); } public getErrorMessage(application: ApplicationStart) { @@ -47,14 +40,20 @@ export class PainlessError extends KbnError { }); } + const rootCause = getRootCause(this.err); + const painlessStack = rootCause?.script_stack ? rootCause?.script_stack.join('\n') : undefined; + return ( <> - {this.message} + {i18n.translate('data.painlessError.painlessScriptedFieldErrorMessage', { + defaultMessage: "Error executing Painless script: '{script}'.", + values: { script: rootCause?.script }, + })} - {this.painlessStack ? ( + {painlessStack ? ( - {this.painlessStack} + {painlessStack} ) : null} @@ -67,21 +66,10 @@ export class PainlessError extends KbnError { } } -function getFailedShards(err: EsError) { - const failedShards = - err.body?.attributes?.error?.failed_shards || - err.body?.attributes?.error?.caused_by?.failed_shards; - return failedShards ? failedShards[0] : undefined; -} - -function getRootCause(err: EsError) { - return getFailedShards(err)?.reason; -} - -export function isPainlessError(err: Error | EsError) { +export function isPainlessError(err: Error | IEsError) { if (!isEsError(err)) return false; - const rootCause = getRootCause(err as EsError); + const rootCause = getRootCause(err as IEsError); if (!rootCause) return false; const { lang } = rootCause; diff --git a/src/plugins/data/public/search/errors/types.ts b/src/plugins/data/public/search/errors/types.ts index 4182209eb68a5..011af015318a7 100644 --- a/src/plugins/data/public/search/errors/types.ts +++ b/src/plugins/data/public/search/errors/types.ts @@ -17,7 +17,7 @@ * under the License. */ -interface FailedShard { +export interface FailedShard { shard: number; index: string; node: string; @@ -39,7 +39,7 @@ interface FailedShard { }; } -export interface EsError { +export interface IEsError { body: { statusCode: number; error: string; @@ -68,6 +68,6 @@ export interface EsError { }; } -export function isEsError(e: any): e is EsError { +export function isEsError(e: any): e is IEsError { return !!e.body?.attributes; } diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts new file mode 100644 index 0000000000000..d07d9b05e91e9 --- /dev/null +++ b/src/plugins/data/public/search/errors/utils.ts @@ -0,0 +1,31 @@ +/* + * 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 { IEsError } from './types'; + +export function getFailedShards(err: IEsError) { + const failedShards = + err.body?.attributes?.error?.failed_shards || + err.body?.attributes?.error?.caused_by?.failed_shards; + return failedShards ? failedShards[0] : undefined; +} + +export function getRootCause(err: IEsError) { + return getFailedShards(err)?.reason; +} diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index e3c6dd3e287d4..1afcf4615ab5a 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -21,6 +21,7 @@ import { get, memoize, trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { getCombinedSignal, AbortError, @@ -31,7 +32,15 @@ import { ISessionService, } from '../../common'; import { SearchUsageCollector } from './collectors'; -import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; +import { + SearchTimeoutError, + PainlessError, + isPainlessError, + TimeoutErrorMode, + isEsError, + EsError, + getHttpError, +} from './errors'; import { toMountPoint } from '../../../kibana_react/public'; export interface SearchInterceptorDeps { @@ -101,8 +110,12 @@ export class SearchInterceptor { } else if (options?.abortSignal?.aborted) { // In the case an application initiated abort, throw the existing AbortError. return e; - } else if (isPainlessError(e)) { - return new PainlessError(e, request); + } else if (isEsError(e)) { + if (isPainlessError(e)) { + return new PainlessError(e, request); + } else { + return new EsError(e); + } } else { return e; } @@ -236,24 +249,28 @@ export class SearchInterceptor { * */ public showError(e: Error) { - if (e instanceof AbortError) return; - - if (e instanceof SearchTimeoutError) { + if (e instanceof AbortError || e instanceof SearchTimeoutError) { // The SearchTimeoutError is shown by the interceptor in getSearchError (regardless of how the app chooses to handle errors) return; - } - - if (e instanceof PainlessError) { + } else if (e instanceof EsError) { this.deps.toasts.addDanger({ - title: 'Search Error', + title: i18n.translate('data.search.esErrorTitle', { + defaultMessage: 'Cannot retrieve search results', + }), text: toMountPoint(e.getErrorMessage(this.application)), }); - return; + } else if (e.constructor.name === 'HttpFetchError') { + this.deps.toasts.addDanger({ + title: i18n.translate('data.search.httpErrorTitle', { + defaultMessage: 'Cannot retrieve your data', + }), + text: toMountPoint(getHttpError(e.message)), + }); + } else { + this.deps.toasts.addError(e, { + title: 'Search Error', + }); } - - this.deps.toasts.addError(e, { - title: 'Search Error', - }); } } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index c17872028ea8d..bc9e2ed6a83ce 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -111,6 +111,7 @@ export default class QueryStringInputUI extends Component { private persistedLog: PersistedLog | undefined; private abortController?: AbortController; + private fetchIndexPatternsAbortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; private queryBarInputDivRefInstance: RefObject = createRef(); @@ -119,7 +120,7 @@ export default class QueryStringInputUI extends Component { return toUser(this.props.query.query); }; - private fetchIndexPatterns = async () => { + private fetchIndexPatterns = debounce(async () => { const stringPatterns = this.props.indexPatterns.filter( (indexPattern) => typeof indexPattern === 'string' ) as string[]; @@ -127,16 +128,26 @@ export default class QueryStringInputUI extends Component { (indexPattern) => typeof indexPattern !== 'string' ) as IIndexPattern[]; + // abort the previous fetch to avoid overriding with outdated data + // issue https://github.com/elastic/kibana/issues/80831 + if (this.fetchIndexPatternsAbortController) this.fetchIndexPatternsAbortController.abort(); + this.fetchIndexPatternsAbortController = new AbortController(); + const currentAbortController = this.fetchIndexPatternsAbortController; + const objectPatternsFromStrings = (await fetchIndexPatterns( this.services.savedObjects!.client, stringPatterns, this.services.uiSettings! )) as IIndexPattern[]; - this.setState({ - indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], - }); - }; + if (!currentAbortController.signal.aborted) { + this.setState({ + indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], + }); + + this.updateSuggestions(); + } + }, 200); private getSuggestions = async () => { if (!this.inputRef) { @@ -506,7 +517,7 @@ export default class QueryStringInputUI extends Component { } this.initPersistedLog(); - this.fetchIndexPatterns().then(this.updateSuggestions); + this.fetchIndexPatterns(); this.handleListUpdate(); window.addEventListener('resize', this.handleAutoHeight); @@ -525,7 +536,7 @@ export default class QueryStringInputUI extends Component { this.initPersistedLog(); if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) { - this.fetchIndexPatterns().then(this.updateSuggestions); + this.fetchIndexPatterns(); } else if (!isEqual(prevProps.query, this.props.query)) { this.updateSuggestions(); } diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index 57c636a9e3c69..e75b8761984ec 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib'; @@ -37,10 +37,12 @@ interface FieldSubType { } export class IndexPatternsFetcher { - private _callDataCluster: LegacyAPICaller; + private elasticsearchClient: ElasticsearchClient; + private allowNoIndices: boolean; - constructor(callDataCluster: LegacyAPICaller) { - this._callDataCluster = callDataCluster; + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { + this.elasticsearchClient = elasticsearchClient; + this.allowNoIndices = allowNoIndices; } /** @@ -55,10 +57,12 @@ export class IndexPatternsFetcher { async getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; - fieldCapsOptions?: { allowNoIndices: boolean }; + fieldCapsOptions?: { allow_no_indices: boolean }; }): Promise { const { pattern, metaFields, fieldCapsOptions } = options; - return await getFieldCapabilities(this._callDataCluster, pattern, metaFields, fieldCapsOptions); + return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, { + allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices, + }); } /** @@ -78,11 +82,11 @@ export class IndexPatternsFetcher { interval: string; }) { const { pattern, lookBack, metaFields } = options; - const { matches } = await resolveTimePattern(this._callDataCluster, pattern); + const { matches } = await resolveTimePattern(this.elasticsearchClient, pattern); const indices = matches.slice(0, lookBack); if (indices.length === 0) { throw createNoMatchingIndicesError(pattern); } - return await getFieldCapabilities(this._callDataCluster, indices, metaFields); + return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js index 8078ea32187b3..fad20a8f0be06 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.test.js @@ -32,36 +32,60 @@ describe('server/index_patterns/service/lib/es_api', () => { afterEach(() => sandbox.restore()); it('calls indices.getAlias() via callCluster', async () => { - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; + await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'indices.getAlias'); + sinon.assert.calledOnce(getAlias); }); it('passes indices directly to es api', async () => { const football = {}; - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; await callIndexAliasApi(callCluster, football); - sinon.assert.calledOnce(callCluster); - expect(callCluster.args[0][1].index).toBe(football); + sinon.assert.calledOnce(getAlias); + expect(getAlias.args[0][0].index).toBe(football); }); it('returns the es response directly', async () => { const football = {}; - const callCluster = sinon.stub().returns(football); + const getAlias = sinon.stub().returns(football); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; const resp = await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(getAlias); expect(resp).toBe(football); }); it('sets ignoreUnavailable and allowNoIndices params', async () => { - const callCluster = sinon.stub(); + const getAlias = sinon.stub(); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; await callIndexAliasApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(getAlias); - const passedOpts = callCluster.args[0][1]; - expect(passedOpts).toHaveProperty('ignoreUnavailable', true); - expect(passedOpts).toHaveProperty('allowNoIndices', false); + const passedOpts = getAlias.args[0][0]; + expect(passedOpts).toHaveProperty('ignore_unavailable', true); + expect(passedOpts).toHaveProperty('allow_no_indices', false); }); it('handles errors with convertEsError()', async () => { @@ -70,9 +94,15 @@ describe('server/index_patterns/service/lib/es_api', () => { const convertedError = new Error('convertedError'); sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError); - const callCluster = sinon.spy(async () => { + const getAlias = sinon.stub(async () => { throw esError; }); + const callCluster = { + indices: { + getAlias, + }, + fieldCaps: sinon.stub(), + }; try { await callIndexAliasApi(callCluster, indices); throw new Error('expected callIndexAliasApi() to throw'); @@ -91,37 +121,60 @@ describe('server/index_patterns/service/lib/es_api', () => { afterEach(() => sandbox.restore()); it('calls fieldCaps() via callCluster', async () => { - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'fieldCaps'); + sinon.assert.calledOnce(fieldCaps); }); it('passes indices directly to es api', async () => { const football = {}; - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster, football); - sinon.assert.calledOnce(callCluster); - expect(callCluster.args[0][1].index).toBe(football); + sinon.assert.calledOnce(fieldCaps); + expect(fieldCaps.args[0][0].index).toBe(football); }); it('returns the es response directly', async () => { const football = {}; - const callCluster = sinon.stub().returns(football); + const fieldCaps = sinon.stub().returns(football); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; const resp = await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(fieldCaps); expect(resp).toBe(football); }); it('sets ignoreUnavailable, allowNoIndices, and fields params', async () => { - const callCluster = sinon.stub(); + const fieldCaps = sinon.stub(); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; await callFieldCapsApi(callCluster); - sinon.assert.calledOnce(callCluster); + sinon.assert.calledOnce(fieldCaps); - const passedOpts = callCluster.args[0][1]; + const passedOpts = fieldCaps.args[0][0]; expect(passedOpts).toHaveProperty('fields', '*'); - expect(passedOpts).toHaveProperty('ignoreUnavailable', true); - expect(passedOpts).toHaveProperty('allowNoIndices', false); + expect(passedOpts).toHaveProperty('ignore_unavailable', true); + expect(passedOpts).toHaveProperty('allow_no_indices', false); }); it('handles errors with convertEsError()', async () => { @@ -130,9 +183,15 @@ describe('server/index_patterns/service/lib/es_api', () => { const convertedError = new Error('convertedError'); sandbox.stub(convertEsErrorNS, 'convertEsError').throws(convertedError); - const callCluster = sinon.spy(async () => { + const fieldCaps = sinon.spy(async () => { throw esError; }); + const callCluster = { + indices: { + getAlias: sinon.stub(), + }, + fieldCaps, + }; try { await callFieldCapsApi(callCluster, indices); throw new Error('expected callFieldCapsApi() to throw'); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts index 27ce14f9a3597..7969324943a9f 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { convertEsError } from './errors'; import { FieldCapsResponse } from './field_capabilities'; @@ -46,15 +46,15 @@ export interface IndexAliasResponse { * @return {Promise} */ export async function callIndexAliasApi( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string[] | string -): Promise { +) { try { - return (await callCluster('indices.getAlias', { + return await callCluster.indices.getAlias({ index: indices, - ignoreUnavailable: true, - allowNoIndices: false, - })) as Promise; + ignore_unavailable: true, + allow_no_indices: false, + }); } catch (error) { throw convertEsError(indices, error); } @@ -73,17 +73,17 @@ export async function callIndexAliasApi( * @return {Promise} */ export async function callFieldCapsApi( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string[] | string, - fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false } + fieldCapsOptions: { allow_no_indices: boolean } = { allow_no_indices: false } ) { try { - return (await callCluster('fieldCaps', { + return await callCluster.fieldCaps({ index: indices, fields: '*', - ignoreUnavailable: true, + ignore_unavailable: true, ...fieldCapsOptions, - })) as FieldCapsResponse; + }); } catch (error) { throw convertEsError(indices, error); } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js index 0e5757b7b782b..2d860dc8b1843 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js @@ -48,9 +48,11 @@ describe('index_patterns/field_capabilities/field_capabilities', () => { }; const stubDeps = (options = {}) => { - const { esResponse = {}, fieldsFromFieldCaps = [], mergeOverrides = identity } = options; + const { esResponse = [], fieldsFromFieldCaps = [], mergeOverrides = identity } = options; - sandbox.stub(callFieldCapsApiNS, 'callFieldCapsApi').callsFake(async () => esResponse); + sandbox + .stub(callFieldCapsApiNS, 'callFieldCapsApi') + .callsFake(async () => ({ body: esResponse })); sandbox.stub(readFieldCapsResponseNS, 'readFieldCapsResponse').returns(fieldsFromFieldCaps); sandbox.stub(mergeOverridesNS, 'mergeOverrides').callsFake(mergeOverrides); }; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 62e77e0adad66..b9e3e8aae0899 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -19,9 +19,9 @@ import { defaults, keyBy, sortBy } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { callFieldCapsApi } from '../es_api'; -import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response'; +import { readFieldCapsResponse } from './field_caps_response'; import { mergeOverrides } from './overrides'; import { FieldDescriptor } from '../../index_patterns_fetcher'; @@ -36,17 +36,13 @@ import { FieldDescriptor } from '../../index_patterns_fetcher'; * @return {Promise>} */ export async function getFieldCapabilities( - callCluster: LegacyAPICaller, + callCluster: ElasticsearchClient, indices: string | string[] = [], metaFields: string[] = [], - fieldCapsOptions?: { allowNoIndices: boolean } + fieldCapsOptions?: { allow_no_indices: boolean } ) { - const esFieldCaps: FieldCapsResponse = await callFieldCapsApi( - callCluster, - indices, - fieldCapsOptions - ); - const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); + const esFieldCaps = await callFieldCapsApi(callCluster, indices, fieldCapsOptions); + const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps.body), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) .filter((name) => !name.startsWith('_')) diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js index 660e9ec30db6a..87f222aaad89d 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.test.js @@ -32,6 +32,11 @@ const TIME_PATTERN = '[logs-]dddd-YYYY.w'; describe('server/index_patterns/service/lib/resolve_time_pattern', () => { let sandbox; + const esClientMock = { + indices: { + getAlias: () => ({}), + }, + }; beforeEach(() => (sandbox = sinon.createSandbox())); afterEach(() => sandbox.restore()); @@ -39,7 +44,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { describe('pre request', () => { it('uses callIndexAliasApi() fn', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({}); - await resolveTimePattern(noop, TIME_PATTERN); + await resolveTimePattern(esClientMock, TIME_PATTERN); sinon.assert.calledOnce(callIndexAliasApi); }); @@ -49,7 +54,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard); - await resolveTimePattern(noop, timePattern); + await resolveTimePattern(esClientMock, timePattern); sinon.assert.calledOnce(timePatternToWildcard); expect(timePatternToWildcard.firstCall.args).toEqual([timePattern]); }); @@ -61,7 +66,7 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({}); sandbox.stub(timePatternToWildcardNS, 'timePatternToWildcard').returns(wildcard); - await resolveTimePattern(noop, timePattern); + await resolveTimePattern(esClientMock, timePattern); sinon.assert.calledOnce(callIndexAliasApi); expect(callIndexAliasApi.firstCall.args[1]).toBe(wildcard); }); @@ -70,13 +75,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { describe('read response', () => { it('returns all aliases names in result.all, ordered by time desc', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({ - 'logs-2016.2': {}, - 'logs-Saturday-2017.1': {}, - 'logs-2016.1': {}, - 'logs-Sunday-2017.1': {}, - 'logs-2015': {}, - 'logs-2016.3': {}, - 'logs-Friday-2017.1': {}, + body: { + 'logs-2016.2': {}, + 'logs-Saturday-2017.1': {}, + 'logs-2016.1': {}, + 'logs-Sunday-2017.1': {}, + 'logs-2015': {}, + 'logs-2016.3': {}, + 'logs-Friday-2017.1': {}, + }, }); const resp = await resolveTimePattern(noop, TIME_PATTERN); @@ -94,13 +101,15 @@ describe('server/index_patterns/service/lib/resolve_time_pattern', () => { it('returns all indices matching the time pattern in matches, ordered by time desc', async () => { sandbox.stub(callIndexAliasApiNS, 'callIndexAliasApi').returns({ - 'logs-2016.2': {}, - 'logs-Saturday-2017.1': {}, - 'logs-2016.1': {}, - 'logs-Sunday-2017.1': {}, - 'logs-2015': {}, - 'logs-2016.3': {}, - 'logs-Friday-2017.1': {}, + body: { + 'logs-2016.2': {}, + 'logs-Saturday-2017.1': {}, + 'logs-2016.1': {}, + 'logs-Sunday-2017.1': {}, + 'logs-2015': {}, + 'logs-2016.3': {}, + 'logs-Friday-2017.1': {}, + }, }); const resp = await resolveTimePattern(noop, TIME_PATTERN); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts index 2e408d7569be5..95ec06dd9c6e6 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -20,7 +20,7 @@ import { chain } from 'lodash'; import moment from 'moment'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { timePatternToWildcard } from './time_pattern_to_wildcard'; import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; @@ -36,10 +36,10 @@ import { callIndexAliasApi, IndicesAliasResponse } from './es_api'; * and the indices that actually match the time * pattern (matches); */ -export async function resolveTimePattern(callCluster: LegacyAPICaller, timePattern: string) { +export async function resolveTimePattern(callCluster: ElasticsearchClient, timePattern: string) { const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern)); - const allIndexDetails = chain(aliases) + const allIndexDetails = chain(aliases.body) .reduce( (acc: string[], index: any, indexName: string) => acc.concat(indexName, Object.keys(index.aliases || {})), diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 428e7fef6deea..041eb235d01e0 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -46,8 +46,8 @@ export function registerRoutes(http: HttpServiceSetup) { }, }, async (context, request, response) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; @@ -105,8 +105,8 @@ export function registerRoutes(http: HttpServiceSetup) { }, }, async (context: RequestHandlerContext, request: any, response: any) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 65313adfc0e0f..97cbb40c13db0 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -26,6 +26,7 @@ import { ISearchSource } from 'src/plugins/data/public'; import { KibanaRequest } from 'src/core/server'; import { LegacyAPICaller } from 'kibana/server'; import { Logger } from 'kibana/server'; +import { Logger as Logger_2 } from 'src/core/server'; import { LoggerFactory } from '@kbn/logging'; import { Moment } from 'moment'; import moment from 'moment'; @@ -658,7 +659,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(callDataCluster: LegacyAPICaller); + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -669,7 +670,7 @@ export class IndexPatternsFetcher { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { - allowNoIndices: boolean; + allow_no_indices: boolean; }; }): Promise; } @@ -1113,8 +1114,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx new file mode 100644 index 0000000000000..2cf626d182eeb --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DiscoverFieldDetails } from './discover_field_details'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; + +const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() +); + +describe('discover sidebar field details', function () { + const defaultProps = { + indexPattern, + details: { buckets: [], error: '', exists: 1, total: true, columns: [] }, + onAddFilter: jest.fn(), + }; + + function mountComponent(field: IndexPatternField) { + const compProps = { ...defaultProps, field }; + return mountWithIntl(); + } + + it('should enable the visualize link for a number field', function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); + }); + + it('should disable the visualize link for an _id field', function () { + const conflictField = new IndexPatternField( + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(conflictField); + expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); + }); + + it('should disable the visualize link for an unknown field', function () { + const unknownField = new IndexPatternField( + { + name: 'test', + type: 'unknown', + esTypes: ['double'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(unknownField); + expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); + }); +}); diff --git a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx index 33724068a6ba8..551caa28d2291 100644 --- a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx +++ b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx @@ -17,37 +17,45 @@ * under the License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { withKnobs, boolean } from '@storybook/addon-knobs'; +import * as React from 'react'; import { PanelOptionsMenu } from '..'; -const euiContextDescriptors = { - id: 'mainMenu', - title: 'Options', - items: [ - { - name: 'Inspect', - icon: 'inspect', - onClick: action('onClick(inspect)'), - }, - { - name: 'Full screen', - icon: 'expand', - onClick: action('onClick(expand)'), +export default { + title: 'components/PanelOptionsMenu', + component: PanelOptionsMenu, + argTypes: { + isViewMode: { + control: { type: 'boolean' }, }, + }, + decorators: [ + (Story: React.ComponentType) => ( +
+ +
+ ), ], }; -storiesOf('components/PanelOptionsMenu', module) - .addDecorator(withKnobs) - .add('default', () => { - const isViewMode = boolean('isViewMode', false); +export function Default({ isViewMode }: React.ComponentProps) { + const euiContextDescriptors = { + id: 'mainMenu', + title: 'Options', + items: [ + { + name: 'Inspect', + icon: 'inspect', + onClick: action('onClick(inspect)'), + }, + { + name: 'Full screen', + icon: 'expand', + onClick: action('onClick(expand)'), + }, + ], + }; - return ( -
- -
- ); - }); + return ; +} +Default.args = { isViewMode: false } as React.ComponentProps; diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss index cdc0f9f0e0451..6b8654f6c3528 100644 --- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss +++ b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss @@ -64,12 +64,12 @@ .embPanel__titleText { @include euiTextTruncate; + font-weight: $euiFontWeightBold; } .embPanel__placeholderTitleText { - @include euiTextTruncate; - font-weight: $euiFontWeightRegular; color: $euiColorMediumShade; + font-weight: $euiFontWeightRegular; } } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index fcf79c1d6b211..c717e4370231e 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { nextTick } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; @@ -343,6 +343,88 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { // expect(action.length).toBe(1); }); +test('Panel title customize link does not exist in view mode', async () => { + const inspector = inspectorPluginMock.createStartContract(); + + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false }, + { getEmbeddableFactory } as any + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Vayon', + lastName: 'Poole', + }); + + const component = mountWithIntl( + Promise.resolve([])} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} + notifications={{} as any} + overlays={{} as any} + application={applicationMock} + inspector={inspector} + SavedObjectFinder={() => null} + /> + ); + + const titleLink = findTestSubject(component, 'embeddablePanelTitleLink'); + expect(titleLink.length).toBe(0); +}); + +test('Runs customize panel action on title click when in edit mode', async () => { + const inspector = inspectorPluginMock.createStartContract(); + + const container = new HelloWorldContainer( + { id: '123', panels: {}, viewMode: ViewMode.EDIT, hidePanelTitles: false }, + { getEmbeddableFactory } as any + ); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Vayon', + lastName: 'Poole', + }); + + const component = mountWithIntl( + Promise.resolve([])} + getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={start.getEmbeddableFactory} + notifications={{} as any} + overlays={{} as any} + application={applicationMock} + inspector={inspector} + SavedObjectFinder={() => null} + /> + ); + + const titleExecute = jest.fn(); + component.setState((s: any) => ({ + ...s, + universalActions: { + ...s.universalActions, + customizePanelTitle: { execute: titleExecute, isCompatible: jest.fn() }, + }, + })); + + const titleLink = findTestSubject(component, 'embeddablePanelTitleLink'); + expect(titleLink.length).toBe(1); + titleLink.simulate('click'); + await nextTick(); + expect(titleExecute).toHaveBeenCalledTimes(1); +}); + test('Updates when hidePanelTitles is toggled', async () => { const inspector = inspectorPluginMock.createStartContract(); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 137f8c24b1fae..1cd48e85469fd 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -76,6 +76,7 @@ interface Props { interface State { panels: EuiContextMenuPanelDescriptor[]; + universalActions: PanelUniversalActions; focusedPanelIndex?: string; viewMode: ViewMode; hidePanelTitle: boolean; @@ -86,6 +87,14 @@ interface State { error?: EmbeddableError; } +interface PanelUniversalActions { + customizePanelTitle: CustomizePanelTitleAction; + addPanel: AddPanelAction; + inspectPanel: InspectPanelAction; + removePanel: RemovePanelAction; + editPanel: EditPanelAction; +} + export class EmbeddablePanel extends React.Component { private embeddableRoot: React.RefObject; private parentSubscription?: Subscription; @@ -102,6 +111,7 @@ export class EmbeddablePanel extends React.Component { Boolean(embeddable.getInput()?.hidePanelTitles); this.state = { + universalActions: this.getUniversalActions(), panels: [], viewMode, hidePanelTitle, @@ -229,6 +239,7 @@ export class EmbeddablePanel extends React.Component { getActionContextMenuPanel={this.getActionContextMenuPanel} hidePanelTitle={this.state.hidePanelTitle} isViewMode={viewOnlyMode} + customizeTitle={this.state.universalActions.customizePanelTitle} closeContextMenu={this.state.closeContextMenu} title={title} badges={this.state.badges} @@ -267,17 +278,7 @@ export class EmbeddablePanel extends React.Component { } }; - private getActionContextMenuPanel = async () => { - let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { - embeddable: this.props.embeddable, - }); - - const { disabledActions } = this.props.embeddable.getInput(); - if (disabledActions) { - const removeDisabledActions = removeById(disabledActions); - regularActions = regularActions.filter(removeDisabledActions); - } - + private getUniversalActions = (): PanelUniversalActions => { const createGetUserData = (overlays: OverlayStart) => async function getUserData(context: { embeddable: IEmbeddable }) { return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => { @@ -299,27 +300,41 @@ export class EmbeddablePanel extends React.Component { }); }; - // These actions are exposed on the context menu for every embeddable, they bypass the trigger + // Universal actions are exposed on the context menu for every embeddable, they bypass the trigger // registry. - const extraActions: Array> = [ - new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), - new AddPanelAction( + return { + customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), + addPanel: new AddPanelAction( this.props.getEmbeddableFactory, this.props.getAllEmbeddableFactories, this.props.overlays, this.props.notifications, this.props.SavedObjectFinder ), - new InspectPanelAction(this.props.inspector), - new RemovePanelAction(), - new EditPanelAction( + inspectPanel: new InspectPanelAction(this.props.inspector), + removePanel: new RemovePanelAction(), + editPanel: new EditPanelAction( this.props.getEmbeddableFactory, this.props.application, this.props.stateTransfer ), - ]; + }; + }; - const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); + private getActionContextMenuPanel = async () => { + let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { + embeddable: this.props.embeddable, + }); + + const { disabledActions } = this.props.embeddable.getInput(); + if (disabledActions) { + const removeDisabledActions = removeById(disabledActions); + regularActions = regularActions.filter(removeDisabledActions); + } + + const sortedActions = [...regularActions, ...Object.values(this.state.universalActions)].sort( + sortByOrderField + ); return await buildContextMenuForActions({ actions: sortedActions.map((action) => ({ diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 9bcef051a9359..44f5c3df2709d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -24,6 +24,7 @@ import { EuiToolTip, EuiScreenReaderOnly, EuiNotificationBadge, + EuiLink, } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; @@ -32,6 +33,7 @@ import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; import { uiToReactComponent } from '../../../../../kibana_react/public'; +import { CustomizePanelTitleAction } from '.'; export interface PanelHeaderProps { title?: string; @@ -44,6 +46,7 @@ export interface PanelHeaderProps { embeddable: IEmbeddable; headerId?: string; showPlaceholderTitle?: boolean; + customizeTitle: CustomizePanelTitleAction; } function renderBadges(badges: Array>, embeddable: IEmbeddable) { @@ -129,6 +132,7 @@ export function PanelHeader({ notifications, embeddable, headerId, + customizeTitle, }: PanelHeaderProps) { const description = getViewDescription(embeddable); const showTitle = !hidePanelTitle && (!isViewMode || title); @@ -172,11 +176,35 @@ export function PanelHeader({ } const renderTitle = () => { - const titleComponent = showTitle ? ( - - {title || placeholderTitle} - - ) : undefined; + let titleComponent; + if (showTitle) { + titleComponent = isViewMode ? ( + + {title || placeholderTitle} + + ) : ( + customizeTitle.execute({ embeddable })} + > + {title || placeholderTitle} + + ); + } return description ? ( { test('creates default ExecutionContext', () => { const execution = createExecution(); expect(execution.context).toMatchObject({ - getInitialInput: expect.any(Function), + getSearchContext: expect.any(Function), variables: expect.any(Object), types: expect.any(Object), }); @@ -143,6 +145,7 @@ describe('Execution', () => { const execution = new Execution({ executor, expression, + params: {}, }); expect(execution.expression).toBe(expression); }); @@ -153,6 +156,7 @@ describe('Execution', () => { const execution = new Execution({ ast: parseExpression(expression), executor, + params: {}, }); expect(execution.expression).toBe(expression); }); @@ -619,7 +623,7 @@ describe('Execution', () => { const execution = new Execution({ executor, ast: parseExpression('add val=1 | throws | add val=3'), - debug: true, + params: { debug: true }, }); execution.start(0); await execution.result; @@ -637,7 +641,7 @@ describe('Execution', () => { const execution = new Execution({ executor, ast: parseExpression('add val=1 | throws | add val=3'), - debug: true, + params: { debug: true }, }); execution.start(0); await execution.result; @@ -658,7 +662,7 @@ describe('Execution', () => { const execution = new Execution({ executor, ast: parseExpression('add val=1 | throws | add val=3'), - debug: true, + params: { debug: true }, }); execution.start(0); await execution.result; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 69140453f486d..2bcf441b14203 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { keys, last, mapValues, reduce, zipObject } from 'lodash'; -import { Executor, ExpressionExecOptions } from '../executor'; +import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; @@ -39,6 +39,7 @@ import { getType, ExpressionValue } from '../expression_types'; import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; +import { ExpressionExecutionParams } from '../service'; const createAbortErrorValue = () => createError({ @@ -46,20 +47,11 @@ const createAbortErrorValue = () => name: 'AbortError', }); -export interface ExecutionParams< - ExtraContext extends Record = Record -> { +export interface ExecutionParams { executor: Executor; ast?: ExpressionAstExpression; expression?: string; - context?: ExtraContext; - - /** - * Whether to execute expression in *debug mode*. In *debug mode* inputs and - * outputs as well as all resolved arguments and time it took to execute each - * function are saved and are available for introspection. - */ - debug?: boolean; + params: ExpressionExecutionParams; } const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ @@ -68,11 +60,10 @@ const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ }); export class Execution< - ExtraContext extends Record = Record, Input = unknown, Output = unknown, - InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object - ? ExtraContext['inspectorAdapters'] + InspectorAdapters extends Adapters = ExpressionExecutionParams['inspectorAdapters'] extends object + ? ExpressionExecutionParams['inspectorAdapters'] : DefaultInspectorAdapters > { /** @@ -92,7 +83,7 @@ export class Execution< * Execution context - object that allows to do side-effects. Context is passed * to every function. */ - public readonly context: ExecutionContext & ExtraContext; + public readonly context: ExecutionContext; /** * AbortController to cancel this Execution. @@ -126,11 +117,10 @@ export class Execution< * can return to other plugins for their consumption. */ public readonly contract: ExecutionContract< - ExtraContext, Input, Output, InspectorAdapters - > = new ExecutionContract(this); + > = new ExecutionContract(this); public readonly expression: string; @@ -142,17 +132,17 @@ export class Execution< return this.context.inspectorAdapters; } - constructor(public readonly params: ExecutionParams) { - const { executor } = params; + constructor(public readonly execution: ExecutionParams) { + const { executor } = execution; - if (!params.ast && !params.expression) { + if (!execution.ast && !execution.expression) { throw new TypeError('Execution params should contain at least .ast or .expression key.'); - } else if (params.ast && params.expression) { + } else if (execution.ast && execution.expression) { throw new TypeError('Execution params cannot contain both .ast and .expression key.'); } - this.expression = params.expression || formatExpression(params.ast!); - const ast = params.ast || parseExpression(this.expression); + this.expression = execution.expression || formatExpression(execution.ast!); + const ast = execution.ast || parseExpression(this.expression); this.state = createExecutionContainer({ ...executor.state.get(), @@ -161,14 +151,13 @@ export class Execution< }); this.context = { - getInitialInput: () => this.input, - variables: {}, + getSearchContext: () => this.execution.params.searchContext || {}, + getSearchSessionId: () => execution.params.searchSessionId, + variables: execution.params.variables || {}, types: executor.getTypes(), abortSignal: this.abortController.signal, - ...(params.context || ({} as ExtraContext)), - inspectorAdapters: (params.context && params.context.inspectorAdapters - ? params.context.inspectorAdapters - : createDefaultInspectorAdapters()) as InspectorAdapters, + inspectorAdapters: execution.params.inspectorAdapters || createDefaultInspectorAdapters(), + ...(execution.params as any).extraContext, }; } @@ -249,10 +238,10 @@ export class Execution< // actually have a `then` function which would be treated as a `Promise`. const { resolvedArgs } = await this.race(this.resolveArgs(fn, input, fnArgs)); args = resolvedArgs; - timeStart = this.params.debug ? now() : 0; + timeStart = this.execution.params.debug ? now() : 0; const output = await this.race(this.invokeFunction(fn, input, resolvedArgs)); - if (this.params.debug) { + if (this.execution.params.debug) { const timeEnd: number = now(); (link as ExpressionAstFunction).debug = { success: true, @@ -267,11 +256,11 @@ export class Execution< if (getType(output) === 'error') return output; input = output; } catch (rawError) { - const timeEnd: number = this.params.debug ? now() : 0; + const timeEnd: number = this.execution.params.debug ? now() : 0; const error = createError(rawError) as ExpressionValueError; error.error.message = `[${fnName}] > ${error.error.message}`; - if (this.params.debug) { + if (this.execution.params.debug) { (link as ExpressionAstFunction).debug = { success: false, fn: fn.name, @@ -404,9 +393,7 @@ export class Execution< const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { return asts.map((item: ExpressionAstExpression) => { return async (subInput = input) => { - const output = await this.interpret(item, subInput, { - debug: this.params.debug, - }); + const output = await this.interpret(item, subInput); if (isExpressionValueError(output)) throw output.error; const casted = this.cast(output, argDefs[argName as any].types); return casted; @@ -438,17 +425,12 @@ export class Execution< return { resolvedArgs }; } - public async interpret( - ast: ExpressionAstNode, - input: T, - options?: ExpressionExecOptions - ): Promise { + public async interpret(ast: ExpressionAstNode, input: T): Promise { switch (getType(ast)) { case 'expression': - const execution = this.params.executor.createExecution( + const execution = this.execution.executor.createExecution( ast as ExpressionAstExpression, - this.context, - options + this.execution.params ); execution.start(input); return await execution.result; diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index c33f8a1a0f36e..856b22470d782 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -30,7 +30,7 @@ const createExecution = ( const execution = new Execution({ executor, ast: parseExpression(expression), - context, + params: { ...context }, }); return execution; }; diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 79bb4c58ab48d..f05f1ded82799 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -25,21 +25,14 @@ import { ExpressionAstExpression } from '../ast'; * `ExecutionContract` is a wrapper around `Execution` class. It provides the * same functionality but does not expose Expressions plugin internals. */ -export class ExecutionContract< - ExtraContext extends Record = Record, - Input = unknown, - Output = unknown, - InspectorAdapters = unknown -> { +export class ExecutionContract { public get isPending(): boolean { const state = this.execution.state.get().state; const finished = state === 'error' || state === 'result'; return !finished; } - constructor( - protected readonly execution: Execution - ) {} + constructor(protected readonly execution: Execution) {} /** * Cancel the execution of the expression. This will set abort signal diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 7c26e586fb790..50475c3bd94ae 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -26,11 +26,11 @@ import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; * `ExecutionContext` is an object available to all functions during a single execution; * it provides various methods to perform side-effects. */ -export interface ExecutionContext { +export interface ExecutionContext { /** - * Get initial input with which execution started. + * Get search context of the expression. */ - getInitialInput: () => Input; + getSearchContext: () => ExecutionContextSearch; /** * Context variables that can be consumed using `var` and `var_set` functions. @@ -55,7 +55,7 @@ export interface ExecutionContext string | undefined; /** * Allows to fetch saved objects from ElasticSearch. In browser `getSavedObject` diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index fd7f5808f0340..85b5589b593af 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -33,6 +33,7 @@ import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableState } from '../../../kibana_utils/common'; +import { ExpressionExecutionParams } from '../service'; export interface ExpressionExecOptions { /** @@ -166,43 +167,34 @@ export class Executor = Record = Record - >( + public async run( ast: string | ExpressionAstExpression, input: Input, - context?: ExtraContext, - options?: ExpressionExecOptions + params: ExpressionExecutionParams = {} ) { - const execution = this.createExecution(ast, context, options); + const execution = this.createExecution(ast, params); execution.start(input); return (await execution.result) as Output; } - public createExecution< - ExtraContext extends Record = Record, - Input = unknown, - Output = unknown - >( + public createExecution( ast: string | ExpressionAstExpression, - context: ExtraContext = {} as ExtraContext, - { debug }: ExpressionExecOptions = {} as ExpressionExecOptions - ): Execution { - const params: ExecutionParams = { + params: ExpressionExecutionParams = {} + ): Execution { + const executionParams: ExecutionParams = { executor: this, - context: { - ...this.context, - ...context, - } as Context & ExtraContext, - debug, + params: { + ...params, + // for canvas we are passing this in, + // canvas should be refactored to not pass any extra context in + extraContext: this.context, + } as any, }; - if (typeof ast === 'string') params.expression = ast; - else params.ast = ast; + if (typeof ast === 'string') executionParams.expression = ast; + else executionParams.ast = ast; - const execution = new Execution(params); + const execution = new Execution(executionParams); return execution; } diff --git a/src/plugins/expressions/common/expression_functions/specs/kibana.ts b/src/plugins/expressions/common/expression_functions/specs/kibana.ts index 2144a8aba2d19..3ec4c23eab28d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/kibana.ts +++ b/src/plugins/expressions/common/expression_functions/specs/kibana.ts @@ -44,15 +44,15 @@ export const kibana: ExpressionFunctionKibana = { args: {}, - fn(input, _, { search = {} }) { + fn(input, _, { getSearchContext }) { const output: ExpressionValueSearchContext = { // TODO: This spread is left here for legacy reasons, possibly Lens uses it. // TODO: But it shouldn't be need. ...input, type: 'kibana_context', - query: [...toArray(search.query), ...toArray((input || {}).query)], - filters: [...(search.filters || []), ...((input || {}).filters || [])], - timeRange: search.timeRange || (input ? input.timeRange : undefined), + query: [...toArray(getSearchContext().query), ...toArray((input || {}).query)], + filters: [...(getSearchContext().filters || []), ...((input || {}).filters || [])], + timeRange: getSearchContext().timeRange || (input ? input.timeRange : undefined), }; return output; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts index e5bd53f63c91d..e5c4b92de4fdb 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/kibana.test.ts @@ -46,8 +46,8 @@ describe('interpreter/functions#kibana', () => { timeRange: { from: '2', to: '3' }, }; context = { - search, - getInitialInput: () => input, + getSearchContext: () => search, + getSearchSessionId: () => undefined, types: {}, variables: {}, abortSignal: {} as any, diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts index 263409f0caca2..88511d4fd571e 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/theme.test.ts @@ -37,7 +37,8 @@ describe('expression_functions', () => { }; context = { - getInitialInput: () => {}, + getSearchContext: () => ({} as any), + getSearchSessionId: () => undefined, types: {}, variables: { theme: themeProps }, abortSignal: {} as any, diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts index ccf49ec918d3d..762f34e3f5566 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var.test.ts @@ -31,7 +31,8 @@ describe('expression_functions', () => { beforeEach(() => { input = { timeRange: { from: '0', to: '1' } }; context = { - getInitialInput: () => input, + getSearchContext: () => input, + getSearchSessionId: () => undefined, types: {}, variables: { test: 1 }, abortSignal: {} as any, diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index b1ae44e6f899e..365ae5b89baea 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -32,7 +32,8 @@ describe('expression_functions', () => { beforeEach(() => { input = { timeRange: { from: '0', to: '1' } }; context = { - getInitialInput: () => input, + getSearchContext: () => input, + getSearchSessionId: () => undefined, types: {}, variables: { test: 1 }, abortSignal: {} as any, diff --git a/src/plugins/expressions/common/mocks.ts b/src/plugins/expressions/common/mocks.ts index 502d88ac955ae..52f96953885cf 100644 --- a/src/plugins/expressions/common/mocks.ts +++ b/src/plugins/expressions/common/mocks.ts @@ -23,7 +23,8 @@ export const createMockExecutionContext = extraContext: ExtraContext = {} as ExtraContext ): ExecutionContext & ExtraContext => { const executionContext: ExecutionContext = { - getInitialInput: jest.fn(), + getSearchContext: jest.fn(), + getSearchSessionId: jest.fn(), variables: {}, types: {}, abortSignal: { @@ -37,7 +38,6 @@ export const createMockExecutionContext = requests: {} as any, data: {} as any, }, - search: {}, }; return { diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 3d0fb968e8a3a..abbba433ab3ca 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Executor, ExpressionExecOptions } from '../executor'; +import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; import { ExecutionContract } from '../execution/execution_contract'; @@ -25,6 +25,8 @@ import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableState } from '../../../kibana_utils/common'; +import { Adapters } from '../../../inspector/common/adapters'; +import { ExecutionContextSearch } from '../execution'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -45,6 +47,23 @@ export type ExpressionsServiceSetup = Pick< | 'fork' >; +export interface ExpressionExecutionParams { + searchContext?: ExecutionContextSearch; + + variables?: Record; + + /** + * Whether to execute expression in *debug mode*. In *debug mode* inputs and + * outputs as well as all resolved arguments and time it took to execute each + * function are saved and are available for introspection. + */ + debug?: boolean; + + searchSessionId?: string; + + inspectorAdapters?: Adapters; +} + /** * The public contract that `ExpressionsService` provides to other plugins * in Kibana Platform in *start* life-cycle. @@ -98,11 +117,10 @@ export interface ExpressionsServiceStart { * expressions.run('...', null, { elasticsearchClient }); * ``` */ - run: = Record>( + run: ( ast: string | ExpressionAstExpression, input: Input, - context?: ExtraContext, - options?: ExpressionExecOptions + params?: ExpressionExecutionParams ) => Promise; /** @@ -110,17 +128,12 @@ export interface ExpressionsServiceStart { * instance that tracks the progress of the execution and can be used to * interact with the execution. */ - execute: < - Input = unknown, - Output = unknown, - ExtraContext extends Record = Record - >( + execute: ( ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, - context?: ExtraContext, - options?: ExpressionExecOptions - ) => ExecutionContract; + params?: ExpressionExecutionParams + ) => ExecutionContract; /** * Create a new instance of `ExpressionsService`. The new instance inherits @@ -214,8 +227,8 @@ export class ExpressionsService implements PersistableState AnyExpressionRenderDefinition) ): void => this.renderers.register(definition); - public readonly run: ExpressionsServiceStart['run'] = (ast, input, context, options) => - this.executor.run(ast, input, context, options); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => + this.executor.run(ast, input, params); public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); @@ -246,8 +259,8 @@ export class ExpressionsService implements PersistableState => this.executor.getTypes(); - public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, context, options) => { - const execution = this.executor.createExecution(ast, context, options); + public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => { + const execution = this.executor.createExecution(ast, params); execution.start(input); return execution.contract; }) as ExpressionsServiceStart['execute']; diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index aef4b73f86e34..91c482621de36 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -145,18 +145,13 @@ export class ExpressionLoader { this.execution.cancel(); } this.setParams(params); - this.execution = getExpressionsService().execute( - expression, - params.context, - { - search: params.searchContext, - variables: params.variables || {}, - inspectorAdapters: params.inspectorAdapters, - }, - { - debug: params.debug, - } - ); + this.execution = getExpressionsService().execute(expression, params.context, { + searchContext: params.searchContext, + variables: params.variables || {}, + inspectorAdapters: params.inspectorAdapters, + searchSessionId: params.searchSessionId, + debug: params.debug, + }); const prevDataHandler = this.execution; const data = await prevDataHandler.getData(); @@ -188,6 +183,9 @@ export class ExpressionLoader { if (params.variables && this.params) { this.params.variables = params.variables; } + if (params.searchSessionId && this.params) { + this.params.searchSessionId = params.searchSessionId; + } this.params.debug = Boolean(params.debug); this.params.inspectorAdapters = (params.inspectorAdapters || diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 4739b9434bdaa..fe95cf5eb0cda 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -90,33 +90,32 @@ export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' export type DatatableRow = Record; // Warning: (ae-forgotten-export) The symbol "Adapters" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExpressionExecutionParams" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DefaultInspectorAdapters" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Execution" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Execution = Record, Input = unknown, Output = unknown, InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters> { - constructor(params: ExecutionParams); +export class Execution { + constructor(execution: ExecutionParams); cancel(): void; // (undocumented) cast(value: any, toTypeNames?: string[]): any; - readonly context: ExecutionContext & ExtraContext; - readonly contract: ExecutionContract; + readonly context: ExecutionContext; + readonly contract: ExecutionContract; + // (undocumented) + readonly execution: ExecutionParams; // (undocumented) readonly expression: string; input: Input; // (undocumented) get inspectorAdapters(): InspectorAdapters; - // Warning: (ae-forgotten-export) The symbol "ExpressionExecOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): Promise; + interpret(ast: ExpressionAstNode, input: T): Promise; // (undocumented) invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; // (undocumented) invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; // (undocumented) - readonly params: ExecutionParams; - // (undocumented) resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; // (undocumented) get result(): Promise; @@ -134,15 +133,15 @@ export type ExecutionContainer = StateContainer { +export interface ExecutionContext { abortSignal: AbortSignal; - getInitialInput: () => Input; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: (type: string, id: string) => Promise>; - inspectorAdapters: InspectorAdapters; // Warning: (ae-forgotten-export) The symbol "ExecutionContextSearch" needs to be exported by the entry point index.d.ts - search?: ExecutionContextSearch; + getSearchContext: () => ExecutionContextSearch; + getSearchSessionId: () => string | undefined; + inspectorAdapters: InspectorAdapters; types: Record; variables: Record; } @@ -150,11 +149,11 @@ export interface ExecutionContext = Record, Input = unknown, Output = unknown, InspectorAdapters = unknown> { - constructor(execution: Execution); +export class ExecutionContract { + constructor(execution: Execution); cancel: () => void; // (undocumented) - protected readonly execution: Execution; + protected readonly execution: Execution; getAst: () => ExpressionAstExpression; getData: () => Promise; getExpression: () => string; @@ -166,16 +165,15 @@ export class ExecutionContract = Re // Warning: (ae-missing-release-tag) "ExecutionParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ExecutionParams = Record> { +export interface ExecutionParams { // (undocumented) ast?: ExpressionAstExpression; // (undocumented) - context?: ExtraContext; - debug?: boolean; - // (undocumented) executor: Executor; // (undocumented) expression?: string; + // (undocumented) + params: ExpressionExecutionParams; } // Warning: (ae-missing-release-tag) "ExecutionState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -198,7 +196,7 @@ export class Executor = Record; // (undocumented) - createExecution = Record, Input = unknown, Output = unknown>(ast: string | ExpressionAstExpression, context?: ExtraContext, { debug }?: ExpressionExecOptions): Execution; + createExecution(ast: string | ExpressionAstExpression, params?: ExpressionExecutionParams): Execution; // (undocumented) static createWithDefaults = Record>(state?: ExecutorState): Executor; // (undocumented) @@ -228,7 +226,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -613,12 +611,12 @@ export type ExpressionsServiceSetup = Pick = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; + execute: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => ExecutionContract; fork: () => ExpressionsService; getFunction: (name: string) => ReturnType; getRenderer: (name: string) => ReturnType; getType: (name: string) => ReturnType; - run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; + run: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise; } // Warning: (ae-missing-release-tag) "ExpressionsSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -894,6 +892,8 @@ export interface IExpressionLoaderParams { // (undocumented) searchContext?: ExecutionContextSearch; // (undocumented) + searchSessionId?: string; + // (undocumented) uiState?: unknown; // (undocumented) variables?: Record; diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 054c5ac3dc467..1643b5734ef1a 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -53,6 +53,7 @@ export interface IExpressionLoaderParams { uiState?: unknown; inspectorAdapters?: Adapters; onRenderError?: RenderErrorHandlerFnType; + searchSessionId?: string; } export interface ExpressionRenderError extends Error { diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index fcdfd5ef3246c..d6925a027358c 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -87,34 +87,33 @@ export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' export type DatatableRow = Record; // Warning: (ae-forgotten-export) The symbol "Adapters" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExpressionExecutionParams" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DefaultInspectorAdapters" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Execution" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class Execution = Record, Input = unknown, Output = unknown, InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters> { - constructor(params: ExecutionParams); +export class Execution { + constructor(execution: ExecutionParams); cancel(): void; // (undocumented) cast(value: any, toTypeNames?: string[]): any; - readonly context: ExecutionContext & ExtraContext; + readonly context: ExecutionContext; // Warning: (ae-forgotten-export) The symbol "ExecutionContract" needs to be exported by the entry point index.d.ts - readonly contract: ExecutionContract; + readonly contract: ExecutionContract; + // (undocumented) + readonly execution: ExecutionParams; // (undocumented) readonly expression: string; input: Input; // (undocumented) get inspectorAdapters(): InspectorAdapters; - // Warning: (ae-forgotten-export) The symbol "ExpressionExecOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - interpret(ast: ExpressionAstNode, input: T, options?: ExpressionExecOptions): Promise; + interpret(ast: ExpressionAstNode, input: T): Promise; // (undocumented) invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise; // (undocumented) invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Promise; // (undocumented) - readonly params: ExecutionParams; - // (undocumented) resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise; // (undocumented) get result(): Promise; @@ -132,15 +131,15 @@ export type ExecutionContainer = StateContainer { +export interface ExecutionContext { abortSignal: AbortSignal; - getInitialInput: () => Input; // Warning: (ae-forgotten-export) The symbol "SavedObjectAttributes" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SavedObject" needs to be exported by the entry point index.d.ts getSavedObject?: (type: string, id: string) => Promise>; - inspectorAdapters: InspectorAdapters; // Warning: (ae-forgotten-export) The symbol "ExecutionContextSearch" needs to be exported by the entry point index.d.ts - search?: ExecutionContextSearch; + getSearchContext: () => ExecutionContextSearch; + getSearchSessionId: () => string | undefined; + inspectorAdapters: InspectorAdapters; types: Record; variables: Record; } @@ -148,16 +147,15 @@ export interface ExecutionContext = Record> { +export interface ExecutionParams { // (undocumented) ast?: ExpressionAstExpression; // (undocumented) - context?: ExtraContext; - debug?: boolean; - // (undocumented) executor: Executor; // (undocumented) expression?: string; + // (undocumented) + params: ExpressionExecutionParams; } // Warning: (ae-missing-release-tag) "ExecutionState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -180,7 +178,7 @@ export class Executor = Record; // (undocumented) - createExecution = Record, Input = unknown, Output = unknown>(ast: string | ExpressionAstExpression, context?: ExtraContext, { debug }?: ExpressionExecOptions): Execution; + createExecution(ast: string | ExpressionAstExpression, params?: ExpressionExecutionParams): Execution; // (undocumented) static createWithDefaults = Record>(state?: ExecutorState): Executor; // (undocumented) @@ -210,7 +208,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts index c45a83588ee44..8cd60e5477d02 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UsageCollectionSetup, CollectorOptions } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, UsageCollectorOptions } from 'src/plugins/usage_collection/server'; import { HttpServiceSetup, CspConfig } from '../../../../../core/server'; interface Usage { @@ -26,7 +26,7 @@ interface Usage { rulesChangedFromDefault: boolean; } -export function createCspCollector(http: HttpServiceSetup): CollectorOptions { +export function createCspCollector(http: HttpServiceSetup): UsageCollectorOptions { return { type: 'csp', isReady: () => true, diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json new file mode 100644 index 0000000000000..d664d936f6667 --- /dev/null +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/*", + "server/**/**/*", + "../../../typings/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/usage_collection/tsconfig.json" }, + ] +} diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json new file mode 100644 index 0000000000000..66244a22336c7 --- /dev/null +++ b/src/plugins/newsfeed/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "common/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" } + ] +} diff --git a/src/plugins/telemetry/schema/legacy_plugins.json b/src/plugins/telemetry/schema/legacy_plugins.json new file mode 100644 index 0000000000000..1a7c0ccb15082 --- /dev/null +++ b/src/plugins/telemetry/schema/legacy_plugins.json @@ -0,0 +1,21 @@ +{ + "properties": { + "localization": { + "properties": { + "locale": { + "type": "keyword" + }, + "integrities": { + "properties": { + "DYNAMIC_KEY": { + "type": "text" + } + } + }, + "labelsCount": { + "type": "long" + } + } + } + } +} diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts index 375fe4f7686c0..875414fbeec48 100644 --- a/src/plugins/usage_collection/server/collector/collector.test.ts +++ b/src/plugins/usage_collection/server/collector/collector.test.ts @@ -122,6 +122,7 @@ describe('collector', () => { type: 'my_test_collector', isReady: () => false, fetch: () => fetchOutput, + schema: { testPass: { type: 'long' } }, }); expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({ type: 'kibana_stats', diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 11a709c037783..951418d448cbd 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -71,17 +71,27 @@ export interface CollectorFetchContext { } export interface CollectorOptions { + /** + * Unique string identifier for the collector + */ type: string; init?: Function; + /** + * Method to return `true`/`false` to confirm if the collector is ready for the `fetch` method to be called. + */ + isReady: () => Promise | boolean; + /** + * Schema definition of the output of the `fetch` method. + */ schema?: MakeSchemaFrom; 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 * a generic example. + * @deprecated Used only by the Legacy Monitoring collection (to be removed in 8.0) */ formatForBulkUpload?: CollectorFormatForBulkUpload; - isReady: () => Promise | boolean; } export class Collector { 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 45a3437777c5f..359a2d214f991 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -20,7 +20,7 @@ import { noop } from 'lodash'; import { Collector } from './collector'; import { CollectorSet } from './collector_set'; -import { UsageCollector } from './usage_collector'; +import { UsageCollector, UsageCollectorOptions } from './usage_collector'; import { elasticsearchServiceMock, loggingSystemMock, @@ -73,8 +73,9 @@ describe('CollectorSet', () => { // Even for Collector vs. UsageCollector new UsageCollector(logger, { type: 'test_duplicated', - fetch: () => 2, + fetch: () => ({ prop: 2 }), isReady: () => false, + schema: { prop: { type: 'long' } }, }) ) ).toThrowError(`Usage collector's type "test_duplicated" is duplicated.`); @@ -252,7 +253,12 @@ describe('CollectorSet', () => { }); describe('isUsageCollector', () => { - const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {}, isReady: () => true }; + const collectorOptions: UsageCollectorOptions = { + type: 'MY_TEST_COLLECTOR', + fetch: () => ({ test: 1 }), + isReady: () => true, + schema: { test: { type: 'long' } }, + }; it('returns true only for UsageCollector instances', () => { const collectors = new CollectorSet({ logger }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 4e64cbc1bf30f..c52830cda6513 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -26,7 +26,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { Collector, CollectorOptions } from './collector'; -import { UsageCollector } from './usage_collector'; +import { UsageCollector, UsageCollectorOptions } from './usage_collector'; interface CollectorSetConfig { logger: Logger; @@ -45,10 +45,22 @@ export class CollectorSet { this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60; } - public makeStatsCollector = (options: CollectorOptions) => { + public makeStatsCollector = < + T, + U, + O extends CollectorOptions = CollectorOptions // Used to allow extra properties (the Collector constructor extends the class with the additional options provided) + >( + options: O + ) => { return new Collector(this.logger, options); }; - public makeUsageCollector = (options: CollectorOptions) => { + public makeUsageCollector = < + T, + U = T, + O extends UsageCollectorOptions = UsageCollectorOptions + >( + options: O + ) => { return new UsageCollector(this.logger, options); }; diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index c294ba77d3cdb..da85f9ab181c9 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -26,4 +26,4 @@ export { CollectorOptions, CollectorFetchContext, } from './collector'; -export { UsageCollector } from './usage_collector'; +export { UsageCollector, UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index bf861a94fccff..5bfc36537e0b0 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -17,13 +17,22 @@ * under the License. */ +import { Logger } from 'src/core/server'; import { KIBANA_STATS_TYPE } from '../../common/constants'; -import { Collector } from './collector'; +import { Collector, CollectorOptions } from './collector'; + +// Enforce the `schema` property for UsageCollectors +export type UsageCollectorOptions = CollectorOptions & + Required, 'schema'>>; export class UsageCollector extends Collector< T, U > { + constructor(protected readonly log: Logger, collectorOptions: UsageCollectorOptions) { + super(log, collectorOptions); + } + protected defaultFormatterForBulkUpload(result: T) { return { type: KIBANA_STATS_TYPE, diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 80e34b1502cda..f7a08fdb5e9dd 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -25,6 +25,7 @@ export { MakeSchemaFrom, SchemaField, CollectorOptions, + UsageCollectorOptions, Collector, CollectorFetchContext, } from './collector'; diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 682d0a071e50d..b52188129f77f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -45,7 +45,7 @@ export async function getFields( payload: {}, pre: { indexPatternsService: new IndexPatternsFetcher( - requestContext.core.elasticsearch.legacy.client.callAsCurrentUser + requestContext.core.elasticsearch.client.asCurrentUser ), }, getUiSettingsService: () => requestContext.core.uiSettings.client, 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 ceae784cf74a6..613f33a47f1f4 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 @@ -50,7 +50,7 @@ describe('AbstractSearchStrategy', () => { expect(fields).toBe(mockedFields); expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({ pattern: indexPattern, - fieldCapsOptions: { allowNoIndices: true }, + fieldCapsOptions: { allow_no_indices: true }, }); }); 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 7b62ad310a354..8b16048f0dce0 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 @@ -86,7 +86,7 @@ export class AbstractSearchStrategy { return await indexPatternsService!.getFieldsForWildcard({ pattern: indexPattern, - fieldCapsOptions: { allowNoIndices: true }, + fieldCapsOptions: { allow_no_indices: true }, }); } diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index c109bb3c6e90c..c88b78948133c 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -53,7 +53,7 @@ export const createVegaFn = ( Input, Arguments, Output, - ExecutionContext + ExecutionContext > => ({ name: 'vega', type: 'render', diff --git a/tasks/config/run.js b/tasks/config/run.js index eddcb0bdd59d0..6e0a3bd08495b 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -176,7 +176,11 @@ module.exports = function () { '--config', 'test/server_integration/http/ssl_redirect/config.js', '--config', - 'test/server_integration/http/cache/config.js', + 'test/server_integration/http/platform/config.ts', + '--config', + 'test/server_integration/http/ssl_with_p12/config.js', + '--config', + 'test/server_integration/http/ssl_with_p12_intermediate/config.js', '--bail', '--debug', '--kibana-install-dir', diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index d45b8f4841cb6..118234d54626c 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - describe('discover tab', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/78689 + describe.skip('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index cc229ef0c2e08..4a14d43aec249 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -117,11 +117,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } else { log.debug(`navigateToUrl ${appUrl}`); await browser.get(appUrl, insertTimestamp); - // accept alert if it pops up - const alert = await browser.getAlert(); - await alert?.accept(); } + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx index b4f9634b23d29..8c62fa246dd59 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/components/main.tsx @@ -63,7 +63,7 @@ class Main extends React.Component<{}, State> { return getExpressions() .execute(expression, context || { type: 'null' }, { inspectorAdapters: adapters, - search: initialContext as any, + searchContext: initialContext as any, }) .getData(); }; diff --git a/test/server_integration/http/cache/index.js b/test/server_integration/http/platform/cache.ts similarity index 92% rename from test/server_integration/http/cache/index.js rename to test/server_integration/http/platform/cache.ts index 5299ce361327e..e39990ca001be 100644 --- a/test/server_integration/http/cache/index.js +++ b/test/server_integration/http/platform/cache.ts @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - +import { FtrProviderContext } from '../../services/types'; // eslint-disable-next-line import/no-default-export -export default function ({ getService }) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('kibana server cache-control', () => { diff --git a/test/server_integration/http/cache/config.js b/test/server_integration/http/platform/config.ts similarity index 76% rename from test/server_integration/http/cache/config.js rename to test/server_integration/http/platform/config.ts index de20bc6fc1f14..00577e092a26a 100644 --- a/test/server_integration/http/cache/config.js +++ b/test/server_integration/http/platform/config.ts @@ -16,16 +16,18 @@ * specific language governing permissions and limitations * under the License. */ +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -export default async function ({ readConfigFile }) { +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { const httpConfig = await readConfigFile(require.resolve('../../config')); return { - testFiles: [require.resolve('./')], + testFiles: [require.resolve('./cache'), require.resolve('./headers')], services: httpConfig.get('services'), servers: httpConfig.get('servers'), junit: { - reportName: 'Http Cache-Control Integration Tests', + reportName: 'Kibana Platform Http Integration Tests', }, esTestCluster: httpConfig.get('esTestCluster'), kbnTestServer: httpConfig.get('kbnTestServer'), diff --git a/test/server_integration/http/platform/headers.ts b/test/server_integration/http/platform/headers.ts new file mode 100644 index 0000000000000..260bc37bd1328 --- /dev/null +++ b/test/server_integration/http/platform/headers.ts @@ -0,0 +1,82 @@ +/* + * 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 Http from 'http'; +import Url from 'url'; +import { FtrProviderContext } from '../../services/types'; + +// @ts-ignore +import getUrl from '../../../../src/test_utils/get_url'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const oneSec = 1_000; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const config = getService('config'); + + describe('headers timeout ', () => { + it('issue-73849', async () => { + const agent = new Http.Agent({ + keepAlive: true, + }); + const { protocol, hostname, port } = Url.parse(getUrl.baseUrl(config.get('servers.kibana'))); + + function performRequest() { + return new Promise((resolve, reject) => { + const req = Http.request( + { + protocol, + hostname, + port, + path: '/', + method: 'GET', + agent, + }, + function (res) { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => resolve(data)); + } + ); + + req.on('socket', (socket) => { + socket.write('GET / HTTP/1.1\r\n'); + setTimeout(() => { + socket.write('Host: localhost\r\n'); + }, oneSec); + setTimeout(() => { + // headersTimeout doesn't seem to fire if request headers are sent in one packet. + socket.write('\r\n'); + req.end(); + }, 2 * oneSec); + }); + + req.on('error', reject); + }); + } + + await performRequest(); + const defaultHeadersTimeout = 40 * oneSec; + await delay(defaultHeadersTimeout + oneSec); + await performRequest(); + }); + }); +} diff --git a/test/server_integration/services/types.d.ts b/test/server_integration/services/types.d.ts new file mode 100644 index 0000000000000..c79c8db57d7df --- /dev/null +++ b/test/server_integration/services/types.d.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services as kibanaCommonServices } from '../../common/services'; +import { services as kibanaApiIntegrationServices } from '../../api_integration/services'; + +export type FtrProviderContext = GenericFtrProviderContext< + typeof kibanaCommonServices & { supertest: typeof kibanaApiIntegrationServices.supertest }, + {} +>; diff --git a/test/tsconfig.json b/test/tsconfig.json index ec6612d0dd00d..a6cc2d34639b7 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -23,6 +23,8 @@ { "path": "../src/plugins/kibana_react/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" } + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 4f22168b67a09..73646291e3d08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,9 @@ "src/plugins/kibana_react/**/*", "src/plugins/usage_collection/**/*", "src/plugins/telemetry_collection_manager/**/*", - "src/plugins/telemetry/**/*" + "src/plugins/telemetry/**/*", + "src/plugins/kibana_usage_collection/**/*", + "src/plugins/newsfeed/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -27,6 +29,8 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" } + { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 4b6ed4a0db813..bb1bdc08cafd6 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -8,5 +8,7 @@ { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/newsfeed/tsconfig.json" }, ] } diff --git a/x-pack/package.json b/x-pack/package.json index d91e11134f9c8..77dc2e662dd28 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -152,7 +152,7 @@ "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", "cronstrue": "^1.51.0", - "cypress": "^5.0.0", + "cypress": "5.4.0", "cypress-multi-reporters": "^1.2.3", "cypress-promise": "^1.1.0", "d3": "3.5.17", diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts index 55c980d5edeb4..314254883b2fd 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -12,9 +12,7 @@ When('the user changes the selected percentile', () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); - getDataTestSubj('uxPercentileSelect').click(); - - getDataTestSubj('p95Percentile').click(); + getDataTestSubj('uxPercentileSelect').select('95'); }); Then(`it displays client metric related to that percentile`, () => { @@ -22,8 +20,5 @@ Then(`it displays client metric related to that percentile`, () => { verifyClientMetrics(metrics, false); - // reset to median - getDataTestSubj('uxPercentileSelect').click(); - - getDataTestSubj('p50Percentile').click(); + getDataTestSubj('uxPercentileSelect').select('50'); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 75a018afa13d7..18cd7d79cc69f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -6,19 +6,14 @@ import React, { useCallback, useEffect } from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; +import { EuiSelect } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { I18LABELS } from '../translations'; const DEFAULT_P = 50; -const StyledSpan = styled.span` - font-weight: 600; -`; - export function UserPercentile() { const history = useHistory(); @@ -49,32 +44,29 @@ export function UserPercentile() { const options = [ { value: '50', - inputDisplay: I18LABELS.percentile50thMedian, + text: I18LABELS.percentile50thMedian, dropdownDisplay: I18LABELS.percentile50thMedian, 'data-test-subj': 'p50Percentile', }, { value: '75', - inputDisplay: {I18LABELS.percentile75th}, + text: I18LABELS.percentile75th, dropdownDisplay: I18LABELS.percentile75th, 'data-test-subj': 'p75Percentile', }, { value: '90', - inputDisplay: {I18LABELS.percentile90th}, - dropdownDisplay: I18LABELS.percentile90th, + text: I18LABELS.percentile90th, 'data-test-subj': 'p90Percentile', }, { value: '95', - inputDisplay: {I18LABELS.percentile95th}, - dropdownDisplay: I18LABELS.percentile95th, + text: I18LABELS.percentile95th, 'data-test-subj': 'p95Percentile', }, { value: '99', - inputDisplay: {I18LABELS.percentile99th}, - dropdownDisplay: I18LABELS.percentile99th, + text: I18LABELS.percentile99th, 'data-test-subj': 'p99Percentile', }, ]; @@ -84,13 +76,12 @@ export function UserPercentile() { }; return ( - onChange(value)} + onChange={(evt) => onChange(evt.target.value)} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 1198c014f5921..77afe92a8f521 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -20,7 +20,7 @@ import { ViewMode, isErrorEmbeddable, } from '../../../../../../../../src/plugins/embeddable/public'; -import { getLayerList } from './LayerList'; +import { useLayerList } from './useLayerList'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; @@ -55,6 +55,8 @@ export function EmbeddedMapComponent() { const mapFilters = useMapFilters(); + const layerList = useLayerList(); + const [embeddable, setEmbeddable] = useState< MapEmbeddable | ErrorEmbeddable | undefined >(); @@ -148,7 +150,7 @@ export function EmbeddedMapComponent() { if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - await embeddableObject.setLayerList(getLayerList()); + await embeddableObject.setLayerList(layerList); } setEmbeddable(embeddableObject); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx index 07b40addedec3..467e31f15a8de 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -18,7 +18,7 @@ import { REGION_NAME, TRANSACTION_DURATION_COUNTRY, TRANSACTION_DURATION_REGION, -} from './LayerList'; +} from './useLayerList'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index 023f5d61a964e..1053dd611d519 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { EuiThemeProvider } from '../../../../../../../observability/public'; import { MapToolTip } from '../MapToolTip'; -import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../LayerList'; +import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; storiesOf('app/RumDashboard/VisitorsRegionMap', module) .addDecorator((storyFn) => {storyFn()}) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts index c45f8b27d7d3e..e564332583375 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/__mocks__/regions_layer.mock.ts @@ -25,6 +25,11 @@ export const mockLayerList = [ id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', term: 'client.geo.country_iso_code', + whereQuery: { + language: 'kuery', + query: + 'transaction.type : "page-load" and service.name : "undefined"', + }, metrics: [ { type: 'avg', @@ -95,6 +100,11 @@ export const mockLayerList = [ id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', indexPatternTitle: 'apm-*', term: 'client.geo.region_iso_code', + whereQuery: { + language: 'kuery', + query: + 'transaction.type : "page-load" and service.name : "undefined"', + }, metrics: [{ type: 'avg', field: 'transaction.duration.us' }], indexPatternId: 'apm_static_index_pattern_id', }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts similarity index 51% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts index eb149ee2a132d..872553452b263 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/LayerList.test.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/useLayerList.test.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { renderHook } from '@testing-library/react-hooks'; import { mockLayerList } from './__mocks__/regions_layer.mock'; -import { getLayerList } from '../LayerList'; +import { useLayerList } from '../useLayerList'; -describe('LayerList', () => { - describe('getLayerList', () => { - test('it returns the region layer', () => { - const layerList = getLayerList(); - expect(layerList).toStrictEqual(mockLayerList); - }); +describe('useLayerList', () => { + test('it returns the region layer', () => { + const { result } = renderHook(() => useLayerList()); + expect(result.current).toStrictEqual(mockLayerList); }); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts similarity index 79% rename from x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts rename to x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index 138a3f4018c65..bc45d58329f49 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/LayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -22,8 +22,14 @@ import { } from '../../../../../../maps/common/constants'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types'; -const ES_TERM_SOURCE: ESTermSourceDescriptor = { +const ES_TERM_SOURCE_COUNTRY: ESTermSourceDescriptor = { type: 'ES_TERM_SOURCE', id: '3657625d-17b0-41ef-99ba-3a2b2938655c', indexPatternTitle: 'apm-*', @@ -39,6 +45,26 @@ const ES_TERM_SOURCE: ESTermSourceDescriptor = { applyGlobalQuery: true, }; +const ES_TERM_SOURCE_REGION: ESTermSourceDescriptor = { + type: 'ES_TERM_SOURCE', + id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', + indexPatternTitle: 'apm-*', + term: 'client.geo.region_iso_code', + metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], + whereQuery: { + query: 'transaction.type : "page-load"', + language: 'kuery', + }, + indexPatternId: APM_STATIC_INDEX_PATTERN_ID, +}; + +const getWhereQuery = (serviceName: string) => { + return { + query: `${TRANSACTION_TYPE} : "${TRANSACTION_PAGE_LOAD}" and ${SERVICE_NAME} : "${serviceName}"`, + language: 'kuery', + }; +}; + export const REGION_NAME = 'region_name'; export const COUNTRY_NAME = 'name'; @@ -56,7 +82,11 @@ interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { sourceDescriptor: EMSFileSourceDescriptor; } -export function getLayerList() { +export function useLayerList() { + const { urlParams } = useUrlParams(); + + const { serviceName } = urlParams; + const baseLayer: LayerDescriptor = { sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, id: 'b7af286d-2580-4f47-be93-9653d594ce7e', @@ -69,6 +99,8 @@ export function getLayerList() { type: 'VECTOR_TILE', }; + ES_TERM_SOURCE_COUNTRY.whereQuery = getWhereQuery(serviceName!); + const getLayerStyle = (fieldName: string): VectorStyleDescriptor => { return { type: 'VECTOR', @@ -119,7 +151,7 @@ export function getLayerList() { joins: [ { leftField: 'iso2', - right: ES_TERM_SOURCE, + right: ES_TERM_SOURCE_COUNTRY, }, ], sourceDescriptor: { @@ -138,18 +170,13 @@ export function getLayerList() { type: 'VECTOR', }; + ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); + const pageLoadDurationByAdminRegionLayer: VectorLayerDescriptor = { joins: [ { leftField: 'region_iso_code', - right: { - type: 'ES_TERM_SOURCE', - id: 'e62a1b9c-d7ff-4fd4-a0f6-0fdc44bb9e41', - indexPatternTitle: 'apm-*', - term: 'client.geo.region_iso_code', - metrics: [{ type: AGG_TYPE.AVG, field: 'transaction.duration.us' }], - indexPatternId: APM_STATIC_INDEX_PATTERN_ID, - }, + right: ES_TERM_SOURCE_REGION, }, ], sourceDescriptor: { @@ -166,6 +193,7 @@ export function getLayerList() { visible: true, type: 'VECTOR', }; + return [ baseLayer, pageLoadDurationByCountryLayer, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx index a5393995f0864..0e517f6bf6212 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boolean, withKnobs } from '@storybook/addon-knobs'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { SyncBadge } from './SyncBadge'; -storiesOf('app/TransactionDetails/SyncBadge', module) - .addDecorator(withKnobs) - .add( - 'example', - () => { - return ; +export default { + title: 'app/TransactionDetails/SyncBadge', + component: SyncBadge, + argTypes: { + sync: { + control: { type: 'inline-radio', options: [true, false, undefined] }, }, - { - showPanel: true, - info: { source: false }, - } - ) - .add( - 'sync=undefined', - () => { - return ; - }, - { info: { source: false } } - ); + }, +}; + +export function Example({ sync }: ComponentProps) { + return ; +} +Example.args = { sync: true } as ComponentProps; diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index c68dc49cd9370..3549f378efced 100644 --- a/x-pack/plugins/apm/scripts/package.json +++ b/x-pack/plugins/apm/scripts/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "dependencies": { - "@elastic/elasticsearch": "7.9.1", + "@elastic/elasticsearch": "7.10.0-rc.1", "@octokit/rest": "^16.35.0", "console-stamp": "^0.2.9", "hdr-histogram-js": "^1.2.0" diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 49030dc8cacc5..cf1f4852002ec 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -5,7 +5,6 @@ */ import LRU from 'lru-cache'; -import { LegacyAPICaller } from '../../../../../../src/core/server'; import { IndexPatternsFetcher, FieldDescriptor, @@ -45,8 +44,7 @@ export const getDynamicIndexPattern = async ({ } const indexPatternsFetcher = new IndexPatternsFetcher( - (...rest: Parameters) => - context.core.elasticsearch.legacy.client.callAsCurrentUser(...rest) + context.core.elasticsearch.client.asCurrentUser ); // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 1c3a770da79f5..45005f3f5e422 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -22,6 +22,7 @@ interface CloudSetupDependencies { export interface CloudSetup { cloudId?: string; + cloudDeploymentUrl?: string; isCloudEnabled: boolean; } @@ -33,7 +34,7 @@ export class CloudPlugin implements Plugin { } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id, resetPasswordUrl } = this.config; + const { id, resetPasswordUrl, deploymentUrl } = this.config; const isCloudEnabled = getIsCloudEnabled(id); if (home) { @@ -45,6 +46,7 @@ export class CloudPlugin implements Plugin { return { cloudId: id, + cloudDeploymentUrl: deploymentUrl, isCloudEnabled, }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 2c833bcfeaf4c..1516aa9096eca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -28,6 +28,7 @@ jest.mock('react-router-dom', () => ({ ...(jest.requireActual('react-router-dom') as object), useHistory: jest.fn(() => mockHistory), useLocation: jest.fn(() => mockLocation), + useParams: jest.fn(() => ({})), })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 374a2420f5ba7..53aa3db00b66a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -11,6 +11,16 @@ export enum ApiTokenTypes { Search = 'search', } +export const CREATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', { + defaultMessage: 'Successfully created key.', +}); +export const UPDATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', { + defaultMessage: 'Successfully updated API Key.', +}); +export const DELETE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', { + defaultMessage: 'Successfully deleted key.', +}); + export const SEARCH_DISPLAY = i18n.translate( 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search', { @@ -81,3 +91,5 @@ export const TOKEN_TYPE_INFO = [ { value: ApiTokenTypes.Private, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Private] }, { value: ApiTokenTypes.Admin, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Admin] }, ]; + +export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx index a265b2c998d39..a9a0dab044351 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -14,6 +14,7 @@ import { Credentials } from './credentials'; import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; import { externalUrl } from '../../../shared/enterprise_search_url'; +import { CredentialsFlyout } from './credentials_flyout'; describe('Credentials', () => { // Kea mocks @@ -71,4 +72,16 @@ describe('Credentials', () => { button.props().onClick(); expect(actions.showCredentialsForm).toHaveBeenCalledTimes(1); }); + + it('will render CredentialsFlyout if shouldShowCredentialsForm is true', () => { + setMockValues({ shouldShowCredentialsForm: true }); + const wrapper = shallow(); + expect(wrapper.find(CredentialsFlyout)).toHaveLength(1); + }); + + it('will NOT render CredentialsFlyout if shouldShowCredentialsForm is false', () => { + setMockValues({ shouldShowCredentialsForm: false }); + const wrapper = shallow(); + expect(wrapper.find(CredentialsFlyout)).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index b9a482ae462d5..c8eae8cc13f5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -24,16 +24,19 @@ import { import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; + import { CredentialsLogic } from './credentials_logic'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { CredentialsList } from './credentials_list'; +import { CredentialsFlyout } from './credentials_flyout'; export const Credentials: React.FC = () => { const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions( CredentialsLogic ); - const { dataLoading } = useValues(CredentialsLogic); + const { dataLoading, shouldShowCredentialsForm } = useValues(CredentialsLogic); useEffect(() => { initializeCredentialsData(); @@ -63,6 +66,7 @@ export const Credentials: React.FC = () => { + {shouldShowCredentialsForm && }

@@ -120,7 +124,8 @@ export const Credentials: React.FC = () => { )} - + + {!!dataLoading ? : } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx new file mode 100644 index 0000000000000..d2e7ff5f32dd4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutBody } from '@elastic/eui'; + +import { CredentialsFlyoutBody } from './body'; + +describe('CredentialsFlyoutBody', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx new file mode 100644 index 0000000000000..2afba633ca892 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.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 { EuiFlyoutBody } from '@elastic/eui'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +export const CredentialsFlyoutBody: React.FC = () => { + return ( + + + Details go here + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx new file mode 100644 index 0000000000000..c31546472b036 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui'; + +import { CredentialsFlyoutFooter } from './footer'; + +describe('CredentialsFlyoutFooter', () => { + const values = { + activeApiTokenExists: false, + }; + const actions = { + hideCredentialsForm: jest.fn(), + onApiTokenChange: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFlyoutFooter)).toHaveLength(1); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonEmpty); + button.simulate('click'); + expect(button.prop('children')).toEqual('Close'); + expect(actions.hideCredentialsForm).toHaveBeenCalled(); + }); + + it('renders action button text for new tokens', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + + expect(button.prop('children')).toEqual('Save'); + }); + + it('renders action button text for existing tokens', () => { + setMockValues({ activeApiTokenExists: true }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + + expect(button.prop('children')).toEqual('Update'); + }); + + it('calls onApiTokenChange on action button press', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + button.simulate('click'); + + expect(actions.onApiTokenChange).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx new file mode 100644 index 0000000000000..e59a75a578ba4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues, useActions } from 'kea'; +import { + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../credentials_logic'; + +export const CredentialsFlyoutFooter: React.FC = () => { + const { hideCredentialsForm, onApiTokenChange } = useActions(CredentialsLogic); + const { activeApiTokenExists } = useValues(CredentialsLogic); + + return ( + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.closeText', { + defaultMessage: 'Close', + })} + + + + + {activeApiTokenExists + ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateText', { + defaultMessage: 'Update', + }) + : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.saveText', { + defaultMessage: 'Save', + })} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx new file mode 100644 index 0000000000000..a8d9505136faa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutHeader } from '@elastic/eui'; + +import { ApiTokenTypes } from '../constants'; +import { IApiToken } from '../types'; + +import { CredentialsFlyoutHeader } from './header'; + +describe('CredentialsFlyoutHeader', () => { + const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + }; + const values = { + activeApiToken: apiToken, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFlyoutHeader)).toHaveLength(1); + expect(wrapper.find('h2').prop('id')).toEqual('credentialsFlyoutTitle'); + expect(wrapper.find('h2').prop('children')).toEqual('Create a new key'); + }); + + it('changes the title text if editing an existing token', () => { + setMockValues({ + activeApiToken: { + ...apiToken, + id: 'some-id', + name: 'search-key', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find('h2').prop('children')).toEqual('Update search-key'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx new file mode 100644 index 0000000000000..f208cd1c5918f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../credentials_logic'; +import { FLYOUT_ARIA_LABEL_ID } from '../constants'; + +export const CredentialsFlyoutHeader: React.FC = () => { + const { activeApiToken } = useValues(CredentialsLogic); + + return ( + + +

+ {activeApiToken.id + ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateTitle', { + defaultMessage: 'Update {tokenName}', + values: { tokenName: activeApiToken.name }, + }) + : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.createTitle', { + defaultMessage: 'Create a new key', + })} +

+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx new file mode 100644 index 0000000000000..16b669c530012 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyout } from '@elastic/eui'; + +import { CredentialsFlyout } from './'; + +describe('CredentialsFlyout', () => { + const actions = { + hideCredentialsForm: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + const flyout = wrapper.find(EuiFlyout); + + expect(flyout).toHaveLength(1); + expect(flyout.prop('aria-labelledby')).toEqual('credentialsFlyoutTitle'); + expect(flyout.prop('onClose')).toEqual(actions.hideCredentialsForm); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx new file mode 100644 index 0000000000000..602a5250716c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useActions } from 'kea'; +import { EuiPortal, EuiFlyout } from '@elastic/eui'; + +import { CredentialsLogic } from '../credentials_logic'; +import { FLYOUT_ARIA_LABEL_ID } from '../constants'; +import { CredentialsFlyoutHeader } from './header'; +import { CredentialsFlyoutBody } from './body'; +import { CredentialsFlyoutFooter } from './footer'; + +export const CredentialsFlyout: React.FC = () => { + const { hideCredentialsForm } = useActions(CredentialsLogic); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 11b1253332cb2..de79862b540ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -6,17 +6,33 @@ import { resetContext } from 'kea'; -import { CredentialsLogic } from './credentials_logic'; -import { ApiTokenTypes } from './constants'; - +import { mockHttpValues } from '../../../__mocks__'; jest.mock('../../../shared/http', () => ({ - HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } }, + HttpLogic: { values: mockHttpValues }, })); -import { HttpLogic } from '../../../shared/http'; +const { http } = mockHttpValues; + jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn() } }, + setSuccessMessage: jest.fn(), flashAPIErrors: jest.fn(), })); -import { flashAPIErrors } from '../../../shared/flash_messages'; +import { + FlashMessagesLogic, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; + +jest.mock('../../app_logic', () => ({ + AppLogic: { + selectors: { myRole: jest.fn(() => ({})) }, + values: { myRole: jest.fn(() => ({})) }, + }, +})); +import { AppLogic } from '../../app_logic'; + +import { ApiTokenTypes } from './constants'; +import { CredentialsLogic } from './credentials_logic'; describe('CredentialsLogic', () => { const DEFAULT_VALUES = { @@ -38,6 +54,7 @@ describe('CredentialsLogic', () => { meta: {}, nameInputBlurred: false, shouldShowCredentialsForm: false, + fullEngineAccessChecked: false, }; const mount = (defaults?: object) => { @@ -952,6 +969,13 @@ describe('CredentialsLogic', () => { }); }); }); + + describe('listener side-effects', () => { + it('should clear flashMessages whenever the credentials form flyout is opened', () => { + CredentialsLogic.actions.showCredentialsForm(); + expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); + }); + }); }); describe('hideCredentialsForm', () => { @@ -1068,10 +1092,10 @@ describe('CredentialsLogic', () => { mount(); jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {}); const promise = Promise.resolve({ meta, results }); - (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + http.get.mockReturnValue(promise); CredentialsLogic.actions.fetchCredentials(2); - expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials', { + expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials', { query: { 'page[current]': 2, }, @@ -1083,7 +1107,7 @@ describe('CredentialsLogic', () => { it('handles errors', async () => { mount(); const promise = Promise.reject('An error occured'); - (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + http.get.mockReturnValue(promise); CredentialsLogic.actions.fetchCredentials(); try { @@ -1101,12 +1125,10 @@ describe('CredentialsLogic', () => { .spyOn(CredentialsLogic.actions, 'setCredentialsDetails') .mockImplementationOnce(() => {}); const promise = Promise.resolve(credentialsDetails); - (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + http.get.mockReturnValue(promise); CredentialsLogic.actions.fetchDetails(); - expect(HttpLogic.values.http.get).toHaveBeenCalledWith( - '/api/app_search/credentials/details' - ); + expect(http.get).toHaveBeenCalledWith('/api/app_search/credentials/details'); await promise; expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith( credentialsDetails @@ -1116,7 +1138,7 @@ describe('CredentialsLogic', () => { it('handles errors', async () => { mount(); const promise = Promise.reject('An error occured'); - (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + http.get.mockReturnValue(promise); CredentialsLogic.actions.fetchDetails(); try { @@ -1134,20 +1156,19 @@ describe('CredentialsLogic', () => { mount(); jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {}); const promise = Promise.resolve(); - (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + http.delete.mockReturnValue(promise); CredentialsLogic.actions.deleteApiKey(tokenName); - expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( - `/api/app_search/credentials/${tokenName}` - ); + expect(http.delete).toHaveBeenCalledWith(`/api/app_search/credentials/${tokenName}`); await promise; expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName); + expect(setSuccessMessage).toHaveBeenCalled(); }); it('handles errors', async () => { mount(); const promise = Promise.reject('An error occured'); - (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + http.delete.mockReturnValue(promise); CredentialsLogic.actions.deleteApiKey(tokenName); try { @@ -1157,9 +1178,189 @@ describe('CredentialsLogic', () => { } }); }); + + describe('onApiTokenChange', () => { + it('calls a POST API endpoint that creates a new token if the active token does not exist yet', async () => { + const createdToken = { + name: 'new-key', + type: ApiTokenTypes.Admin, + }; + mount({ + activeApiToken: createdToken, + }); + jest.spyOn(CredentialsLogic.actions, 'onApiTokenCreateSuccess'); + const promise = Promise.resolve(createdToken); + http.post.mockReturnValue(promise); + + CredentialsLogic.actions.onApiTokenChange(); + expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', { + body: JSON.stringify(createdToken), + }); + await promise; + expect(CredentialsLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('calls a PUT endpoint that updates the active token if it already exists', async () => { + const updatedToken = { + name: 'test-key', + type: ApiTokenTypes.Private, + read: true, + write: false, + access_all_engines: false, + engines: ['engine1'], + }; + mount({ + activeApiToken: { + ...updatedToken, + id: 'some-id', + }, + }); + jest.spyOn(CredentialsLogic.actions, 'onApiTokenUpdateSuccess'); + const promise = Promise.resolve(updatedToken); + http.put.mockReturnValue(promise); + + CredentialsLogic.actions.onApiTokenChange(); + expect(http.put).toHaveBeenCalledWith('/api/app_search/credentials/test-key', { + body: JSON.stringify(updatedToken), + }); + await promise; + expect(CredentialsLogic.actions.onApiTokenUpdateSuccess).toHaveBeenCalledWith(updatedToken); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + http.post.mockReturnValue(promise); + + CredentialsLogic.actions.onApiTokenChange(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + + describe('token type data', () => { + it('does not send extra read/write/engine access data for admin tokens', () => { + const correctAdminToken = { + name: 'bogus-admin', + type: ApiTokenTypes.Admin, + }; + const extraData = { + read: true, + write: true, + access_all_engines: true, + }; + mount({ activeApiToken: { ...correctAdminToken, ...extraData } }); + + CredentialsLogic.actions.onApiTokenChange(); + expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', { + body: JSON.stringify(correctAdminToken), + }); + }); + + it('does not send extra read/write access data for search tokens', () => { + const correctSearchToken = { + name: 'bogus-search', + type: ApiTokenTypes.Search, + access_all_engines: false, + engines: ['some-engine'], + }; + const extraData = { + read: true, + write: false, + }; + mount({ activeApiToken: { ...correctSearchToken, ...extraData } }); + + CredentialsLogic.actions.onApiTokenChange(); + expect(http.post).toHaveBeenCalledWith('/api/app_search/credentials', { + body: JSON.stringify(correctSearchToken), + }); + }); + + // Private tokens send all data per the PUT test above. + // If that ever changes, we should capture that in another test here. + }); + }); + + describe('onEngineSelect', () => { + it('calls addEngineName if the engine is not selected', () => { + mount({ + activeApiToken: { + ...DEFAULT_VALUES.activeApiToken, + engines: [], + }, + }); + jest.spyOn(CredentialsLogic.actions, 'addEngineName'); + + CredentialsLogic.actions.onEngineSelect('engine1'); + expect(CredentialsLogic.actions.addEngineName).toHaveBeenCalledWith('engine1'); + expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine1']); + }); + + it('calls removeEngineName if the engine is already selected', () => { + mount({ + activeApiToken: { + ...DEFAULT_VALUES.activeApiToken, + engines: ['engine1', 'engine2'], + }, + }); + jest.spyOn(CredentialsLogic.actions, 'removeEngineName'); + + CredentialsLogic.actions.onEngineSelect('engine1'); + expect(CredentialsLogic.actions.removeEngineName).toHaveBeenCalledWith('engine1'); + expect(CredentialsLogic.values.activeApiToken.engines).toEqual(['engine2']); + }); + }); }); describe('selectors', () => { + describe('fullEngineAccessChecked', () => { + it('should be true if active token is set to access all engines and the user can access all engines', () => { + (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({ + canAccessAllEngines: true, + }); + mount({ + activeApiToken: { + ...DEFAULT_VALUES.activeApiToken, + access_all_engines: true, + }, + }); + + expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(true); + }); + + it('should be false if the token is not set to access all engines', () => { + (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({ + canAccessAllEngines: true, + }); + mount({ + activeApiToken: { + ...DEFAULT_VALUES.activeApiToken, + access_all_engines: false, + }, + }); + + expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false); + }); + + it('should be false if the user cannot acess all engines', () => { + (AppLogic.selectors.myRole as jest.Mock).mockReturnValueOnce({ + canAccessAllEngines: false, + }); + mount({ + activeApiToken: { + ...DEFAULT_VALUES.activeApiToken, + access_all_engines: true, + }, + }); + + expect(CredentialsLogic.values.fullEngineAccessChecked).toEqual(false); + }); + }); + describe('activeApiTokenExists', () => { it('should be false if the token has no id', () => { mount({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index c6f929c45eb23..30b5fabc4d0c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -7,11 +7,17 @@ import { kea, MakeLogicType } from 'kea'; import { formatApiName } from '../../utils/format_api_name'; -import { ApiTokenTypes } from './constants'; +import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants'; import { HttpLogic } from '../../../shared/http'; +import { + FlashMessagesLogic, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; +import { AppLogic } from '../../app_logic'; + import { IMeta } from '../../../../../common/types'; -import { flashAPIErrors } from '../../../shared/flash_messages'; import { IEngine } from '../../types'; import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types'; @@ -23,9 +29,7 @@ const defaultApiToken: IApiToken = { access_all_engines: true, }; -// TODO CREATE_MESSAGE, UPDATE_MESSAGE, and DELETE_MESSAGE from ent-search - -export interface ICredentialsLogicActions { +interface ICredentialsLogicActions { addEngineName(engineName: string): string; onApiKeyDelete(tokenName: string): string; onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken; @@ -46,9 +50,11 @@ export interface ICredentialsLogicActions { fetchCredentials(page?: number): number; fetchDetails(): { value: boolean }; deleteApiKey(tokenName: string): string; + onApiTokenChange(): void; + onEngineSelect(engineName: string): string; } -export interface ICredentialsLogicValues { +interface ICredentialsLogicValues { activeApiToken: IApiToken; activeApiTokenExists: boolean; activeApiTokenRawName: string; @@ -79,10 +85,7 @@ export const CredentialsLogic = kea< setCredentialsData: (meta, apiTokens) => ({ meta, apiTokens }), setCredentialsDetails: (details) => details, setNameInputBlurred: (nameInputBlurred) => nameInputBlurred, - setTokenReadWrite: ({ name, checked }) => ({ - name, - checked, - }), + setTokenReadWrite: ({ name, checked }) => ({ name, checked }), setTokenName: (name) => name, setTokenType: (tokenType) => tokenType, showCredentialsForm: (apiToken = { ...defaultApiToken }) => apiToken, @@ -92,6 +95,8 @@ export const CredentialsLogic = kea< fetchCredentials: (page) => page, fetchDetails: true, deleteApiKey: (tokenName) => tokenName, + onApiTokenChange: () => null, + onEngineSelect: (engineName) => engineName, }), reducers: () => ({ apiTokens: [ @@ -204,7 +209,11 @@ export const CredentialsLogic = kea< ], }), selectors: ({ selectors }) => ({ - // TODO fullEngineAccessChecked from ent-search + fullEngineAccessChecked: [ + () => [AppLogic.selectors.myRole, selectors.activeApiToken], + (myRole, activeApiToken) => + !!(myRole.canAccessAllEngines && activeApiToken.access_all_engines), + ], dataLoading: [ () => [selectors.isCredentialsDetailsComplete, selectors.isCredentialsDataComplete], (isCredentialsDetailsComplete, isCredentialsDataComplete) => { @@ -217,6 +226,9 @@ export const CredentialsLogic = kea< ], }), listeners: ({ actions, values }) => ({ + showCredentialsForm: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, initializeCredentialsData: () => { actions.fetchCredentials(); actions.fetchDetails(); @@ -247,11 +259,50 @@ export const CredentialsLogic = kea< await http.delete(`/api/app_search/credentials/${tokenName}`); actions.onApiKeyDelete(tokenName); + setSuccessMessage(DELETE_MESSAGE); } catch (e) { flashAPIErrors(e); } }, - // TODO onApiTokenChange from ent-search - // TODO onEngineSelect from ent-search + onApiTokenChange: async () => { + const { id, name, engines, type, read, write } = values.activeApiToken; + + const data: IApiToken = { + name, + type, + }; + if (type === ApiTokenTypes.Private) { + data.read = read; + data.write = write; + } + if (type !== ApiTokenTypes.Admin) { + data.access_all_engines = values.fullEngineAccessChecked; + data.engines = engines; + } + + try { + const { http } = HttpLogic.values; + const body = JSON.stringify(data); + + if (id) { + const response = await http.put(`/api/app_search/credentials/${name}`, { body }); + actions.onApiTokenUpdateSuccess(response); + setSuccessMessage(UPDATE_MESSAGE); + } else { + const response = await http.post('/api/app_search/credentials', { body }); + actions.onApiTokenCreateSuccess(response); + setSuccessMessage(CREATE_MESSAGE); + } + } catch (e) { + flashAPIErrors(e); + } + }, + onEngineSelect: (engineName: string) => { + if (values.activeApiToken?.engines?.includes(engineName)) { + actions.removeEngineName(engineName); + } else { + actions.addEngineName(engineName); + } + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index f1f16d1a6f7a4..f2bdc1a8c75b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -4,28 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/kea.mock'; +import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; -import { ProductSelector } from './'; +import { SetupGuideCta } from '../setup_guide'; import { ProductCard } from '../product_card'; +import { ProductSelector } from './'; + describe('ProductSelector', () => { - it('renders the overview page and product cards with no host set', () => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + it('renders the overview page, product cards, & setup guide CTAs with no host set', () => { + setMockValues({ config: { host: '' } }); const wrapper = shallow(); expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); expect(wrapper.find(ProductCard)).toHaveLength(2); + expect(wrapper.find(SetupGuideCta)).toHaveLength(1); }); describe('access checks when host is set', () => { beforeEach(() => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); + setMockValues({ config: { host: 'localhost' } }); }); it('does not render the App Search card if the user does not have access to AS', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 6d76b741d7a97..235ececd8b6fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -3,11 +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. */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { useValues } from 'kea'; @@ -30,6 +25,7 @@ import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kiba import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; @@ -66,9 +62,13 @@ export const ProductSelector: React.FC = ({ access }) =>

- {i18n.translate('xpack.enterpriseSearch.overview.subheading', { - defaultMessage: 'Select a product to get started', - })} + {config.host + ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { + defaultMessage: 'Select a product to get started.', + }) + : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', { + defaultMessage: 'Choose a product to set up and get started.', + })}

@@ -87,6 +87,7 @@ export const ProductSelector: React.FC = ({ access }) => )} + {!config.host && } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts index c367424d375f9..89f7da4547569 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts @@ -5,3 +5,4 @@ */ export { SetupGuide } from './setup_guide'; +export { SetupGuideCta } from './setup_guide_cta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss new file mode 100644 index 0000000000000..103ef8eccb558 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.enterpriseSearchSetupCta { + margin: $euiSize auto $euiSizeXL; + + // Clickable EuiPanel override - line panel up with product cards + &.euiPanel--isClickable { + width: calc(100% - #{$euiSize}); + } + + &__text { + max-width: $euiSize * 40; + } + + &__image { + display: block; + max-width: 100%; + width: $euiSize * 10; + margin: 0 auto; + + @include euiBreakpoint('xs', 's') { + width: $euiSize * 15; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx new file mode 100644 index 0000000000000..f235beef3b337 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.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 { shallow } from 'enzyme'; + +import { SetupGuideCta } from './'; + +describe('SetupGuideCta', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('.enterpriseSearchSetupCta')).toHaveLength(1); + expect(wrapper.find('img')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx new file mode 100644 index 0000000000000..2a0e2ffc34f3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; +import { EuiPanel } from '../../../shared/react_router_helpers'; + +import CtaImage from './assets/getting_started.png'; +import './setup_guide_cta.scss'; + +export const SetupGuideCta: React.FC = () => ( + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.setupCta.title', { + defaultMessage: 'Enterprise-grade functionality for teams big and small', + })} +

+
+ + {i18n.translate('xpack.enterpriseSearch.overview.setupCta.description', { + defaultMessage: + 'Add search to your app or internal organization with Elastic App Search and Workplace Search. Watch the video to see what you can do when search is made easy.', + })} + +
+ + + +
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 803d2c8462b1b..0e929c9191e0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -6,10 +6,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPage } from '@elastic/eui'; -import '../__mocks__/kea.mock'; -import { useValues } from 'kea'; +import { setMockValues } from '../__mocks__/kea.mock'; import { EnterpriseSearch } from './'; import { SetupGuide } from './components/setup_guide'; @@ -18,7 +16,7 @@ import { ProductSelector } from './components/product_selector'; describe('EnterpriseSearch', () => { it('renders the Setup Guide and Product Selector', () => { - (useValues as jest.Mock).mockReturnValue({ + setMockValues({ errorConnecting: false, config: { host: 'localhost' }, }); @@ -28,15 +26,23 @@ describe('EnterpriseSearch', () => { expect(wrapper.find(ProductSelector)).toHaveLength(1); }); - it('renders the error connecting prompt when host is not configured', () => { - (useValues as jest.Mock).mockReturnValueOnce({ + it('renders the error connecting prompt only if host is configured', () => { + setMockValues({ errorConnecting: true, - config: { host: '' }, + config: { host: 'localhost' }, }); const wrapper = shallow(); expect(wrapper.find(ErrorConnecting)).toHaveLength(1); - expect(wrapper.find(EuiPage)).toHaveLength(0); expect(wrapper.find(ProductSelector)).toHaveLength(0); + + setMockValues({ + errorConnecting: true, + config: { host: '' }, + }); + wrapper.setProps({}); // Re-render + + expect(wrapper.find(ErrorConnecting)).toHaveLength(0); + expect(wrapper.find(ProductSelector)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 7b97c6c9e58b6..048baabe6a1dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -25,7 +25,7 @@ export const EnterpriseSearch: React.FC = ({ access = {} }) => const { errorConnecting } = useValues(HttpLogic); const { config } = useValues(KibanaLogic); - const showErrorConnecting = config.host && errorConnecting; + const showErrorConnecting = !!(config.host && errorConnecting); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 82fbb8940d460..3a4585b6d9a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -8,18 +8,18 @@ import '../../__mocks__/kea.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { EuiLink, EuiButton } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiPanel } from '@elastic/eui'; import { mockKibanaValues, mockHistory } from '../../__mocks__'; -import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; +import { EuiReactRouterLink, EuiReactRouterButton, EuiReactRouterPanel } from './eui_link'; describe('EUI & React Router Component Helpers', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('renders', () => { + it('renders an EuiLink', () => { const wrapper = shallow(); expect(wrapper.find(EuiLink)).toHaveLength(1); @@ -31,6 +31,13 @@ describe('EUI & React Router Component Helpers', () => { expect(wrapper.find(EuiButton)).toHaveLength(1); }); + it('renders an EuiPanel', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(EuiPanel).prop('paddingSize')).toEqual('l'); + }); + it('passes down all ...rest props', () => { const wrapper = shallow(); const link = wrapper.find(EuiLink); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index f9f6ec54e8832..78546911813ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -6,14 +6,15 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps, EuiPanel } from '@elastic/eui'; +import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; import { KibanaLogic } from '../kibana'; import { HttpLogic } from '../http'; import { letBrowserHandleEvent, createHref } from './'; /** - * Generates either an EuiLink or EuiButton with a React-Router-ified link + * Generates EUI components with React-Router-ified links * * Based off of EUI's recommendations for handling React Router: * https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51 @@ -54,9 +55,11 @@ export const EuiReactRouterHelper: React.FC = ({ return React.cloneElement(children as React.ReactElement, reactRouterProps); }; -type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; -type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; +/** + * Component helpers + */ +type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; export const EuiReactRouterLink: React.FC = ({ to, onClick, @@ -68,6 +71,7 @@ export const EuiReactRouterLink: React.FC = ({ ); +type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; export const EuiReactRouterButton: React.FC = ({ to, onClick, @@ -78,3 +82,15 @@ export const EuiReactRouterButton: React.FC = ({ ); + +type TEuiReactRouterPanelProps = EuiPanelProps & IEuiReactRouterProps; +export const EuiReactRouterPanel: React.FC = ({ + to, + onClick, + shouldNotCreateHref, + ...rest +}) => ( + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts index 6915d3222c45c..36fb0560d7323 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -6,5 +6,8 @@ export { letBrowserHandleEvent } from './link_events'; export { createHref, ICreateHrefOptions } from './create_href'; -export { EuiReactRouterLink as EuiLink } from './eui_link'; -export { EuiReactRouterButton as EuiButton } from './eui_link'; +export { + EuiReactRouterLink as EuiLink, + EuiReactRouterButton as EuiButton, + EuiReactRouterPanel as EuiPanel, +} from './eui_link'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts deleted file mode 100644 index 05c60ebced088..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { useDidUpdateEffect } from './use_did_update_effect'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx deleted file mode 100644 index e3d2ffb44f01e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.test.tsx +++ /dev/null @@ -1,33 +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, { useState } from 'react'; -import { mount } from 'enzyme'; - -import { EuiLink } from '@elastic/eui'; - -import { useDidUpdateEffect } from './use_did_update_effect'; - -const fn = jest.fn(); - -const TestHook = ({ value }: { value: number }) => { - const [inputValue, setValue] = useState(value); - useDidUpdateEffect(fn, [inputValue]); - return setValue(2)} />; -}; - -const wrapper = mount(); - -describe('useDidUpdateEffect', () => { - it('should not fire function when value unchanged', () => { - expect(fn).not.toHaveBeenCalled(); - }); - - it('should fire function when value changed', () => { - wrapper.find(EuiLink).simulate('click'); - expect(fn).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx deleted file mode 100644 index 4c3e10fc84b84..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/use_did_update_effect/use_did_update_effect.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * Sometimes we don't want to fire the initial useEffect call. - * This custom Hook only fires after the intial render has completed. - */ -import { useEffect, useRef, DependencyList } from 'react'; - -export const useDidUpdateEffect = (fn: Function, inputs: DependencyList) => { - const didMountRef = useRef(false); - - useEffect(() => { - if (didMountRef.current) { - fn(); - } else { - didMountRef.current = true; - } - }, inputs); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 7789d0caba345..c305ae9d5f7a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -20,7 +20,7 @@ export const contentSources = [ boost: 1, }, { - id: '123', + id: '124', serviceType: 'jira', searchable: true, supportedByLicense: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts new file mode 100644 index 0000000000000..18e7851485222 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/group_logic.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IGroupValues } from '../group_logic'; + +import { IGroupDetails, ISourcePriority } from '../../../types'; + +export const mockGroupValues = { + group: {} as IGroupDetails, + dataLoading: true, + manageUsersModalVisible: false, + managerModalFormErrors: [], + sharedSourcesModalVisible: false, + confirmDeleteModalVisible: false, + groupNameInputValue: '', + selectedGroupSources: [], + selectedGroupUsers: [], + groupPrioritiesUnchanged: true, + activeSourcePriorities: {} as ISourcePriority, + cachedSourcePriorities: {} as ISourcePriority, +} as IGroupValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts new file mode 100644 index 0000000000000..0483799b73093 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IGroupsValues } from '../groups_logic'; + +import { IContentSource, IUser, IGroup } from '../../../types'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +export const mockGroupsValues = { + groups: [] as IGroup[], + contentSources: [] as IContentSource[], + users: [] as IUser[], + groupsDataLoading: true, + groupListLoading: true, + newGroupModalOpen: false, + newGroupName: '', + hasFiltersSet: false, + newGroup: null, + newGroupNameErrors: [], + filterSourcesDropdownOpen: false, + filteredSources: [], + filterUsersDropdownOpen: false, + filteredUsers: [], + allGroupUsersLoading: false, + allGroupUsers: [], + filterValue: '', + groupsMeta: DEFAULT_META, +} as IGroupsValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts new file mode 100644 index 0000000000000..b4724c8ed3f0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -0,0 +1,509 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { + FlashMessagesLogic, + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, +} from '../../../shared/flash_messages'; + +jest.mock('../../../shared/kibana', () => ({ + KibanaLogic: { values: { navigateToUrl: jest.fn() } }, +})); +import { KibanaLogic } from '../../../shared/kibana'; + +import { groups } from '../../__mocks__/groups.mock'; +import { mockGroupValues } from './__mocks__/group_logic.mock'; +import { GroupLogic } from './group_logic'; + +import { GROUPS_PATH } from '../../routes'; + +describe('GroupLogic', () => { + const group = groups[0]; + const sourceIds = ['123', '124']; + const userIds = ['1z1z']; + const sourcePriorities = { [sourceIds[0]]: 1, [sourceIds[1]]: 0.5 }; + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupLogic.values).toEqual(mockGroupValues); + }); + + describe('actions', () => { + describe('onInitializeGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.onInitializeGroup(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.groupNameInputValue).toEqual(group.name); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(true); + }); + }); + + describe('onGroupNameChanged', () => { + it('sets reducers', () => { + const renamedGroup = { + ...group, + name: 'changed', + }; + GroupLogic.actions.onGroupNameChanged(renamedGroup); + + expect(GroupLogic.values.group).toEqual(renamedGroup); + expect(GroupLogic.values.groupNameInputValue).toEqual(renamedGroup.name); + }); + }); + + describe('onGroupPrioritiesChanged', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupPrioritiesChanged(group); + + expect(GroupLogic.values.dataLoading).toEqual(false); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupNameInputChange', () => { + it('sets reducers', () => { + const name = 'new name'; + GroupLogic.actions.onGroupNameInputChange(name); + + expect(GroupLogic.values.groupNameInputValue).toEqual(name); + }); + }); + + describe('addGroupSource', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupSource', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupSource(sourceIds[0]); + GroupLogic.actions.addGroupSource(sourceIds[1]); + GroupLogic.actions.removeGroupSource(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupSources).toEqual([sourceIds[1]]); + }); + }); + + describe('addGroupUser', () => { + it('sets reducer', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[0]]); + }); + }); + + describe('removeGroupUser', () => { + it('sets reducers', () => { + GroupLogic.actions.addGroupUser(sourceIds[0]); + GroupLogic.actions.addGroupUser(sourceIds[1]); + GroupLogic.actions.removeGroupUser(sourceIds[0]); + + expect(GroupLogic.values.selectedGroupUsers).toEqual([sourceIds[1]]); + }); + }); + + describe('onGroupSourcesSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupSourcesSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + expect(GroupLogic.values.cachedSourcePriorities).toEqual(sourcePriorities); + expect(GroupLogic.values.activeSourcePriorities).toEqual(sourcePriorities); + }); + }); + + describe('onGroupUsersSaved', () => { + it('sets reducers', () => { + GroupLogic.actions.onGroupUsersSaved(group); + + expect(GroupLogic.values.group).toEqual(group); + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('setGroupModalErrors', () => { + it('sets reducers', () => { + const errors = ['this is an error']; + GroupLogic.actions.setGroupModalErrors(errors); + + expect(GroupLogic.values.managerModalFormErrors).toEqual(errors); + }); + }); + + describe('hideSharedSourcesModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideSharedSourcesModal(group); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(false); + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('hideManageUsersModal', () => { + it('sets reducers', () => { + GroupLogic.actions.hideManageUsersModal(group); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(false); + expect(GroupLogic.values.managerModalFormErrors).toEqual([]); + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('selectAllSources', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllSources(group.contentSources); + + expect(GroupLogic.values.selectedGroupSources).toEqual(sourceIds); + }); + }); + + describe('selectAllUsers', () => { + it('sets reducers', () => { + GroupLogic.actions.selectAllUsers(group.users); + + expect(GroupLogic.values.selectedGroupUsers).toEqual(userIds); + }); + }); + + describe('updatePriority', () => { + it('sets reducers', () => { + const PRIORITY_VALUE = 4; + GroupLogic.actions.updatePriority(sourceIds[0], PRIORITY_VALUE); + + expect(GroupLogic.values.activeSourcePriorities).toEqual({ + [sourceIds[0]]: PRIORITY_VALUE, + }); + expect(GroupLogic.values.groupPrioritiesUnchanged).toEqual(false); + }); + }); + + describe('resetGroup', () => { + it('sets reducers', () => { + GroupLogic.actions.resetGroup(); + + expect(GroupLogic.values.group).toEqual({}); + expect(GroupLogic.values.dataLoading).toEqual(true); + }); + }); + + describe('hideConfirmDeleteModal', () => { + it('sets reducer', () => { + GroupLogic.actions.showConfirmDeleteModal(); + GroupLogic.actions.hideConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroup', () => { + it('calls API and sets values', async () => { + const onInitializeGroupSpy = jest.spyOn(GroupLogic.actions, 'onInitializeGroup'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups/123'); + await promise; + expect(onInitializeGroupSpy).toHaveBeenCalledWith(group); + }); + + it('handles 404 error', async () => { + const promise = Promise.reject({ response: { status: 404 } }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'Unable to find group with ID: "123".', + }); + } + }); + + it('handles non-404 error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.initializeGroup(sourceIds[0]); + + try { + await promise; + } catch { + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(FlashMessagesLogic.actions.setQueuedMessages).toHaveBeenCalledWith({ + type: 'error', + message: 'this is an error', + }); + } + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('deletes a group', async () => { + const promise = Promise.resolve(true); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( + '/api/workplace_search/groups/123' + ); + + await promise; + expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Group "group" was successfully deleted.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.deleteGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('updateGroupName', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.onGroupNameInputChange('new name'); + }); + it('updates name', async () => { + const onGroupNameChangedSpy = jest.spyOn(GroupLogic.actions, 'onGroupNameChanged'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith('/api/workplace_search/groups/123', { + body: JSON.stringify({ group: { name: 'new name' } }), + }); + + await promise; + expect(onGroupNameChangedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully renamed this group to "group".' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.updateGroupName(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSources', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + GroupLogic.actions.selectAllSources(group.contentSources); + }); + it('updates name', async () => { + const onGroupSourcesSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupSourcesSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/share', + { + body: JSON.stringify({ content_source_ids: sourceIds }), + } + ); + + await promise; + expect(onGroupSourcesSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared content sources.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSources(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupUsers', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupUsersSavedSpy = jest.spyOn(GroupLogic.actions, 'onGroupUsersSaved'); + const promise = Promise.resolve(group); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/assign', + { + body: JSON.stringify({ user_ids: userIds }), + } + ); + + await promise; + expect(onGroupUsersSavedSpy).toHaveBeenCalledWith(group); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated the users of this group.' + ); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupUsers(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveGroupSourcePrioritization', () => { + beforeEach(() => { + GroupLogic.actions.onInitializeGroup(group); + }); + it('updates name', async () => { + const onGroupPrioritiesChangedSpy = jest.spyOn( + GroupLogic.actions, + 'onGroupPrioritiesChanged' + ); + const promise = Promise.resolve(group); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + expect(HttpLogic.values.http.put).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/boosts', + { + body: JSON.stringify({ + content_source_boosts: [ + [sourceIds[0], 1], + [sourceIds[1], 0.5], + ], + }), + } + ); + + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Successfully updated shared source prioritization.' + ); + expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.put as jest.Mock).mockReturnValue(promise); + + GroupLogic.actions.saveGroupSourcePrioritization(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('showConfirmDeleteModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showConfirmDeleteModal(); + + expect(GroupLogic.values.confirmDeleteModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showSharedSourcesModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showSharedSourcesModal(); + + expect(GroupLogic.values.sharedSourcesModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('showManageUsersModal', () => { + it('sets reducer and clears flash messages', () => { + GroupLogic.actions.showManageUsersModal(); + + expect(GroupLogic.values.manageUsersModalVisible).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetFlashMessages', () => { + it('clears flash messages', () => { + GroupLogic.actions.resetFlashMessages(); + + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 1ce0fe53726d4..b895200d3fc22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -56,13 +56,11 @@ export interface IGroupActions { } export interface IGroupValues { - contentSources: IContentSourceDetails[]; - users: IUser[]; group: IGroupDetails; dataLoading: boolean; manageUsersModalVisible: boolean; managerModalFormErrors: string[]; - sharedSourcesModalModalVisible: boolean; + sharedSourcesModalVisible: boolean; confirmDeleteModalVisible: boolean; groupNameInputValue: string; selectedGroupSources: string[]; @@ -138,7 +136,7 @@ export const GroupLogic = kea>({ hideManageUsersModal: () => [], }, ], - sharedSourcesModalModalVisible: [ + sharedSourcesModalVisible: [ false, { showSharedSourcesModal: () => true, @@ -225,8 +223,7 @@ export const GroupLogic = kea>({ } ); - const error = e.response.status === 404 ? NOT_FOUND_MESSAGE : e; - + const error = e.response?.status === 404 ? NOT_FOUND_MESSAGE : e; FlashMessagesLogic.actions.setQueuedMessages({ type: 'error', message: error, @@ -321,7 +318,7 @@ export const GroupLogic = kea>({ const GROUP_USERS_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated', { - defaultMessage: 'Successfully updated the users of this group', + defaultMessage: 'Successfully updated the users of this group.', } ); setSuccessMessage(GROUP_USERS_UPDATED_MESSAGE); @@ -353,7 +350,7 @@ export const GroupLogic = kea>({ const GROUP_PRIORITIZATION_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupPrioritizationUpdated', { - defaultMessage: 'Successfully updated shared source prioritization', + defaultMessage: 'Successfully updated shared source prioritization.', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx new file mode 100644 index 0000000000000..6f293920fa387 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { groups } from '../../__mocks__/groups.mock'; + +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { GroupOverview } from './components/group_overview'; +import { GroupSourcePrioritization } from './components/group_source_prioritization'; + +import { GroupRouter } from './group_router'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { ManageUsersModal } from './components/manage_users_modal'; +import { SharedSourcesModal } from './components/shared_sources_modal'; + +describe('GroupRouter', () => { + const initializeGroup = jest.fn(); + const resetGroup = jest.fn(); + + beforeEach(() => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: groups[0], + }); + + setMockActions({ + initializeGroup, + resetGroup, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FlashMessages)).toHaveLength(1); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupOverview)).toHaveLength(1); + expect(wrapper.find(GroupSourcePrioritization)).toHaveLength(1); + }); + + it('renders modals', () => { + setMockValues({ + sharedSourcesModalVisible: true, + manageUsersModalVisible: true, + group: groups[0], + }); + + const wrapper = shallow(); + + expect(wrapper.find(ManageUsersModal)).toHaveLength(1); + expect(wrapper.find(SharedSourcesModal)).toHaveLength(1); + }); + + it('handles breadcrumbs while loading', () => { + setMockValues({ + sharedSourcesModalVisible: false, + manageUsersModalVisible: false, + group: {}, + }); + + const loadingBreadcrumbs = ['Groups', '...']; + + const wrapper = shallow(); + + const firstBreadCrumb = wrapper.find(SetPageChrome).first(); + const lastBreadCrumb = wrapper.find(SetPageChrome).last(); + + expect(firstBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, 'Source Prioritization']); + expect(lastBreadCrumb.prop('trail')).toEqual(loadingBreadcrumbs); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 0a637497a5b05..1b6f0c4c49a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { Route, Switch, useParams } from 'react-router-dom'; -import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; @@ -26,16 +26,13 @@ import { GroupSourcePrioritization } from './components/group_source_prioritizat export const GroupRouter: React.FC = () => { const { groupId } = useParams() as { groupId: string }; - const { messages } = useValues(FlashMessagesLogic); const { initializeGroup, resetGroup } = useActions(GroupLogic); const { - sharedSourcesModalModalVisible, + sharedSourcesModalVisible, manageUsersModalVisible, group: { name }, } = useValues(GroupLogic); - const hasMessages = messages.length > 0; - useEffect(() => { initializeGroup(groupId); return resetGroup; @@ -43,7 +40,7 @@ export const GroupRouter: React.FC = () => { return ( <> - {hasMessages && } + @@ -55,7 +52,7 @@ export const GroupRouter: React.FC = () => { - {sharedSourcesModalModalVisible && } + {sharedSourcesModalVisible && } {manageUsersModalVisible && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx new file mode 100644 index 0000000000000..98f40acb96469 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions, setMockValues } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Groups } from './groups'; + +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { Loading } from '../../components/shared/loading'; +import { FlashMessages } from '../../../shared/flash_messages'; + +import { AddGroupModal } from './components/add_group_modal'; +import { ClearFiltersLink } from './components/clear_filters_link'; +import { GroupsTable } from './components/groups_table'; +import { TableFilters } from './components/table_filters'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton as EuiLinkButton } from '../../../shared/react_router_helpers'; + +const getSearchResults = jest.fn(); +const openNewGroupModal = jest.fn(); +const resetGroups = jest.fn(); +const setFilterValue = jest.fn(); +const setActivePage = jest.fn(); + +const mockMeta = { + ...DEFAULT_META, + page: { + current: 1, + total_results: 50, + total_pages: 5, + }, +}; + +const mockSuccessMessage = { + type: 'success', + message: 'group added', +}; + +const mockValues = { + groups, + groupsDataLoading: false, + newGroupModalOpen: false, + newGroup: null, + groupListLoading: false, + hasFiltersSet: false, + groupsMeta: mockMeta, + filteredSources: [], + filteredUsers: [], + filterValue: '', + isFederatedAuth: false, +}; + +describe('GroupOverview', () => { + beforeEach(() => { + setMockActions({ + getSearchResults, + openNewGroupModal, + resetGroups, + setFilterValue, + setActivePage, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(GroupsTable)).toHaveLength(1); + expect(wrapper.find(TableFilters)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, groupsDataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('gets search results when filters changed', () => { + const wrapper = shallow(); + + const filters = wrapper.find(TableFilters).dive().shallow(); + const input = filters.find(EuiFieldSearch); + + input.simulate('change', { target: { value: 'Query' } }); + + expect(getSearchResults).toHaveBeenCalledWith(true); + }); + + it('renders manage button when new group added', () => { + setMockValues({ + ...mockValues, + newGroup: { name: 'group', id: '123' }, + messages: [mockSuccessMessage], + }); + const wrapper = shallow(); + const flashMessages = wrapper.find(FlashMessages).dive().shallow(); + + expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1); + }); + + it('renders ClearFiltersLink when filters set', () => { + setMockValues({ + ...mockValues, + hasFiltersSet: true, + groupsMeta: DEFAULT_META, + }); + + const wrapper = shallow(); + + expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); + }); + + it('renders inviteUsersButton when not federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: false, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1); + expect(action.find(EuiLinkButton)).toHaveLength(1); + }); + + it('does not render inviteUsersButton when federated auth', () => { + setMockValues({ + ...mockValues, + isFederatedAuth: true, + }); + + const wrapper = shallow(); + + const Action: React.FC = () => + wrapper.find(ViewContentHeader).props().action as React.ReactElement | null; + const action = shallow(); + + expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(0); + }); + + it('renders EuiLoadingSpinner when loading', () => { + setMockValues({ + ...mockValues, + groupListLoading: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + }); + + it('renders AddGroupModal', () => { + setMockValues({ + ...mockValues, + newGroupModalOpen: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(AddGroupModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 34a66282a312d..ae87ef735bb9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -19,7 +19,6 @@ import { ViewContentHeader } from '../../components/shared/view_content_header'; import { getGroupPath, USERS_PATH } from '../../routes'; -import { useDidUpdateEffect } from '../../../shared/use_did_update_effect'; import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; import { GroupsLogic } from './groups_logic'; @@ -40,7 +39,7 @@ export const Groups: React.FC = () => { groupListLoading, hasFiltersSet, groupsMeta: { - page: { current: activePage, total_results: numGroups }, + page: { total_results: numGroups }, }, filteredSources, filteredUsers, @@ -56,18 +55,17 @@ export const Groups: React.FC = () => { return resetGroups; }, [filteredSources, filteredUsers, filterValue]); - // Because the initial search happens above, we want to skip the initial search and use the custom hook to do so. - useDidUpdateEffect(() => { - getSearchResults(); - }, [activePage]); - if (groupsDataLoading) { return ; } if (newGroup && hasMessages) { messages[0].description = ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action', { defaultMessage: 'Manage Group', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts new file mode 100644 index 0000000000000..18206573a3978 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -0,0 +1,432 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { + values: { http: { get: jest.fn(), post: jest.fn() } }, + }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn() } }, + flashAPIErrors: jest.fn(), + setSuccessMessage: jest.fn(), + setQueuedSuccessMessage: jest.fn(), +})); +import { FlashMessagesLogic, flashAPIErrors } from '../../../shared/flash_messages'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { JSON_HEADER as headers } from '../../../../../common/constants'; + +import { groups } from '../../__mocks__/groups.mock'; +import { contentSources } from '../../__mocks__/content_sources.mock'; +import { users } from '../../__mocks__/users.mock'; +import { mockGroupsValues } from './__mocks__/groups_logic.mock'; +import { GroupsLogic } from './groups_logic'; + +// We need to mock out the debounced functionality +const TIMEOUT = 400; +const delay = () => new Promise((resolve) => setTimeout(resolve, TIMEOUT)); + +describe('GroupsLogic', () => { + const clearFlashMessagesSpy = jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); + const groupsResponse = { + results: groups, + meta: DEFAULT_META, + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + GroupsLogic.mount(); + }); + + it('has expected default values', () => { + expect(GroupsLogic.values).toEqual(mockGroupsValues); + }); + + describe('actions', () => { + describe('onInitializeGroups', () => { + it('sets reducers', () => { + GroupsLogic.actions.onInitializeGroups({ contentSources, users }); + + expect(GroupsLogic.values.groupsDataLoading).toEqual(false); + expect(GroupsLogic.values.contentSources).toEqual(contentSources); + expect(GroupsLogic.values.users).toEqual(users); + }); + }); + + describe('setSearchResults', () => { + it('sets reducers', () => { + GroupsLogic.actions.setSearchResults(groupsResponse); + + expect(GroupsLogic.values.groups).toEqual(groups); + expect(GroupsLogic.values.groupListLoading).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('addFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredSource', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredSource('foo'); + GroupsLogic.actions.addFilteredSource('bar'); + GroupsLogic.actions.addFilteredSource('baz'); + GroupsLogic.actions.removeFilteredSource('foo'); + + expect(GroupsLogic.values.filteredSources).toEqual(['bar', 'baz']); + }); + }); + + describe('addFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz', 'foo']); + }); + }); + + describe('removeFilteredUser', () => { + it('sets reducers', () => { + GroupsLogic.actions.addFilteredUser('foo'); + GroupsLogic.actions.addFilteredUser('bar'); + GroupsLogic.actions.addFilteredUser('baz'); + GroupsLogic.actions.removeFilteredUser('foo'); + + expect(GroupsLogic.values.filteredUsers).toEqual(['bar', 'baz']); + }); + }); + + describe('setGroupUsers', () => { + it('sets reducers', () => { + GroupsLogic.actions.setGroupUsers(users); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(false); + expect(GroupsLogic.values.allGroupUsers).toEqual(users); + }); + }); + + describe('setAllGroupLoading', () => { + it('sets reducer', () => { + GroupsLogic.actions.setAllGroupLoading(true); + + expect(GroupsLogic.values.allGroupUsersLoading).toEqual(true); + expect(GroupsLogic.values.allGroupUsers).toEqual([]); + }); + }); + + describe('setFilterValue', () => { + it('sets reducer', () => { + GroupsLogic.actions.setFilterValue('foo'); + + expect(GroupsLogic.values.filterValue).toEqual('foo'); + }); + }); + + describe('setNewGroupName', () => { + it('sets reducer', () => { + const NEW_NAME = 'new name'; + GroupsLogic.actions.setNewGroupName(NEW_NAME); + + expect(GroupsLogic.values.newGroupName).toEqual(NEW_NAME); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('setNewGroup', () => { + it('sets reducer', () => { + GroupsLogic.actions.setNewGroup(groups[0]); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroup).toEqual(groups[0]); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + }); + }); + + describe('setNewGroupFormErrors', () => { + it('sets reducer', () => { + const errors = ['this is an error']; + GroupsLogic.actions.setNewGroupFormErrors(errors); + + expect(GroupsLogic.values.newGroupNameErrors).toEqual(errors); + }); + }); + + describe('closeNewGroupModal', () => { + it('sets reducer', () => { + GroupsLogic.actions.closeNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(false); + expect(GroupsLogic.values.newGroupName).toEqual(''); + expect(GroupsLogic.values.newGroupNameErrors).toEqual([]); + }); + }); + + describe('closeFilterSourcesDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterSourcesDropdown(); + GroupsLogic.actions.closeFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(false); + }); + }); + + describe('closeFilterUsersDropdown', () => { + it('sets reducer', () => { + // Open dropdown first + GroupsLogic.actions.toggleFilterUsersDropdown(); + GroupsLogic.actions.closeFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(false); + }); + }); + + describe('setGroupsLoading', () => { + it('sets reducer', () => { + // Set to false first + GroupsLogic.actions.setSearchResults(groupsResponse); + GroupsLogic.actions.setGroupsLoading(); + + expect(GroupsLogic.values.groupListLoading).toEqual(true); + }); + }); + + describe('resetGroups', () => { + it('sets reducer', () => { + GroupsLogic.actions.resetGroups(); + + expect(GroupsLogic.values.newGroup).toEqual(null); + }); + }); + }); + + describe('listeners', () => { + describe('initializeGroups', () => { + it('calls API and sets values', async () => { + const onInitializeGroupsSpy = jest.spyOn(GroupsLogic.actions, 'onInitializeGroups'); + const promise = Promise.resolve(groupsResponse); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/workplace_search/groups'); + await promise; + expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.initializeGroups(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('getSearchResults', () => { + const search = { + query: '', + content_source_ids: [], + user_ids: [], + }; + + const payload = { + body: JSON.stringify({ + page: { + current: 1, + size: 10, + }, + search, + }), + headers, + }; + + it('calls API and sets values', async () => { + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('calls API and resets pagination', async () => { + // Set active page to 2 to confirm resetting sends the `payload` value of 1 for the current page. + GroupsLogic.actions.setActivePage(2); + const setSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'setSearchResults'); + const promise = Promise.resolve(groups); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(true); + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith( + '/api/workplace_search/groups/search', + payload + ); + await promise; + expect(setSearchResultsSpy).toHaveBeenCalledWith(groups); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.getSearchResults(); + try { + await promise; + } catch { + // Account for `breakpoint` that debounces filter value. + await delay(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('fetchGroupUsers', () => { + it('calls API and sets values', async () => { + const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); + const promise = Promise.resolve(users); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/workplace_search/groups/123/group_users' + ); + await promise; + expect(setGroupUsersSpy).toHaveBeenCalledWith(users); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.fetchGroupUsers('123'); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('saveNewGroup', () => { + it('calls API and sets values', async () => { + const GROUP_NAME = 'new group'; + GroupsLogic.actions.setNewGroupName(GROUP_NAME); + const setNewGroupSpy = jest.spyOn(GroupsLogic.actions, 'setNewGroup'); + const promise = Promise.resolve(groups[0]); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + expect(HttpLogic.values.http.post).toHaveBeenCalledWith('/api/workplace_search/groups', { + body: JSON.stringify({ group_name: GROUP_NAME }), + headers, + }); + await promise; + expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + (HttpLogic.values.http.post as jest.Mock).mockReturnValue(promise); + + GroupsLogic.actions.saveNewGroup(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + } + }); + }); + + describe('setActivePage', () => { + it('sets reducer', () => { + const getSearchResultsSpy = jest.spyOn(GroupsLogic.actions, 'getSearchResults'); + const activePage = 3; + GroupsLogic.actions.setActivePage(activePage); + + expect(GroupsLogic.values.groupsMeta).toEqual({ + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: activePage, + }, + }); + + expect(getSearchResultsSpy).toHaveBeenCalled(); + }); + }); + + describe('openNewGroupModal', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.openNewGroupModal(); + + expect(GroupsLogic.values.newGroupModalOpen).toEqual(true); + expect(GroupsLogic.values.newGroup).toEqual(null); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('resetGroupsFilters', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.resetGroupsFilters(); + + expect(GroupsLogic.values.filteredSources).toEqual([]); + expect(GroupsLogic.values.filteredUsers).toEqual([]); + expect(GroupsLogic.values.filterValue).toEqual(''); + expect(GroupsLogic.values.groupsMeta).toEqual(DEFAULT_META); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterSourcesDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterSourcesDropdown(); + + expect(GroupsLogic.values.filterSourcesDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + + describe('toggleFilterUsersDropdown', () => { + it('sets reducer and clears flash messages', () => { + GroupsLogic.actions.toggleFilterUsersDropdown(); + + expect(GroupsLogic.values.filterUsersDropdownOpen).toEqual(true); + expect(clearFlashMessagesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index 35d4387b4cf3d..685a2651cb24a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -335,6 +335,9 @@ export const GroupsLogic = kea>({ flashAPIErrors(e); } }, + setActivePage: () => { + actions.getSearchResults(); + }, openNewGroupModal: () => { FlashMessagesLogic.actions.clearFlashMessages(); }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx new file mode 100644 index 0000000000000..0b2b1ad05dfd7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { GroupsRouter } from './groups_router'; + +import { GroupRouter } from './group_router'; +import { Groups } from './groups'; + +describe('GroupsRouter', () => { + const initializeGroups = jest.fn(); + + beforeEach(() => { + setMockActions({ initializeGroups }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + expect(wrapper.find(GroupRouter)).toHaveLength(1); + expect(wrapper.find(Groups)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index adfa10d37c524..a4fe472065d90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -37,7 +37,9 @@ export const GroupsRouter: React.FC = () => { - + + + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index 6b5f4a05b3aa6..357b49de93412 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -41,6 +41,115 @@ describe('credentials routes', () => { }); }); + describe('POST /api/app_search/credentials', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'post', payload: 'body' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/collection', + }); + }); + + describe('validates', () => { + describe('admin keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'admin-key', + type: 'admin', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on unnecessary properties', () => { + const request = { + body: { + name: 'admin-key', + type: 'admin', + read: true, + access_all_engines: true, + }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('private keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'private-key', + type: 'private', + read: true, + write: false, + access_all_engines: false, + engines: ['engine1', 'engine2'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on missing keys', () => { + const request = { + body: { + name: 'private-key', + type: 'private', + }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('search keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + access_all_engines: true, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on missing keys', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + }, + }; + mockRouter.shouldThrow(request); + }); + + it('throws on extra keys', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + read: true, + write: false, + access_all_engines: false, + engines: ['engine1', 'engine2'], + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); + }); + describe('GET /api/app_search/credentials/details', () => { let mockRouter: MockRouter; @@ -61,6 +170,123 @@ describe('credentials routes', () => { }); }); + describe('PUT /api/app_search/credentials/{name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + const mockRequest = { + params: { + name: 'abc123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/abc123', + }); + }); + + describe('validates', () => { + describe('admin keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'admin-key', + type: 'admin', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on unnecessary properties', () => { + const request = { + body: { + name: 'admin-key', + type: 'admin', + read: true, + access_all_engines: true, + }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('private keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'private-key', + type: 'private', + read: true, + write: false, + access_all_engines: false, + engines: ['engine1', 'engine2'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on missing keys', () => { + const request = { + body: { + name: 'private-key', + type: 'private', + }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('search keys', () => { + it('correctly', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + access_all_engines: true, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('throws on missing keys', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + }, + }; + mockRouter.shouldThrow(request); + }); + + it('throws on extra keys', () => { + const request = { + body: { + name: 'search-key', + type: 'search', + read: true, + write: false, + access_all_engines: false, + engines: ['engine1', 'engine2'], + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); + }); + describe('DELETE /api/app_search/credentials/{name}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts index 0f2c1133192c5..85d213c82dd05 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts @@ -8,10 +8,32 @@ import { schema } from '@kbn/config-schema'; import { IRouteDependencies } from '../../plugin'; +const tokenSchema = schema.oneOf([ + schema.object({ + name: schema.string(), + type: schema.literal('admin'), + }), + schema.object({ + name: schema.string(), + type: schema.literal('private'), + read: schema.boolean(), + write: schema.boolean(), + access_all_engines: schema.boolean(), + engines: schema.maybe(schema.arrayOf(schema.string())), + }), + schema.object({ + name: schema.string(), + type: schema.literal('search'), + access_all_engines: schema.boolean(), + engines: schema.maybe(schema.arrayOf(schema.string())), + }), +]); + export function registerCredentialsRoutes({ router, enterpriseSearchRequestHandler, }: IRouteDependencies) { + // Credentials API router.get( { path: '/api/app_search/credentials', @@ -25,6 +47,19 @@ export function registerCredentialsRoutes({ path: '/as/credentials/collection', }) ); + router.post( + { + path: '/api/app_search/credentials', + validate: { + body: tokenSchema, + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/collection', + }) + ); + + // TODO: It would be great to remove this someday router.get( { path: '/api/app_search/credentials/details', @@ -34,6 +69,24 @@ export function registerCredentialsRoutes({ path: '/as/credentials/details', }) ); + + // Single credential API + router.put( + { + path: '/api/app_search/credentials/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: tokenSchema, + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/credentials/${request.params.name}`, + })(context, request, response); + } + ); router.delete( { path: '/api/app_search/credentials/{name}', 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 d9af20763657b..a88c6ebc47683 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 @@ -792,11 +792,12 @@ describe('edit policy', () => { httpRequestsMockHelpers.setPoliciesResponse(policies); }); - describe('with legacy data role config', () => { + describe('with deprecated 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'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, isUsingDeprecatedDataRoleConfig: true, }); const rendered = mountWithIntl(component); @@ -830,6 +831,8 @@ describe('edit policy', () => { expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); }); test('should show cloud notice when cold tier nodes do not exist', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts index 8a1acf72949e6..f6b9506f9068c 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts @@ -6,13 +6,13 @@ // Order of node roles matters here, the warm phase prefers allocating data // to the data_warm role. -import { NodeDataRole, PhaseWithAllocation } from '../types'; +import { DataTierRole, PhaseWithAllocation } from '../types'; -const WARM_PHASE_NODE_PREFERENCE: NodeDataRole[] = ['data_warm', 'data_hot']; +const WARM_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_warm', 'data_hot']; -const COLD_PHASE_NODE_PREFERENCE: NodeDataRole[] = ['data_cold', 'data_warm', 'data_hot']; +const COLD_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_cold', 'data_warm', 'data_hot']; -export const phaseToNodePreferenceMap: Record = Object.freeze({ +export const phaseToNodePreferenceMap: Record = Object.freeze({ warm: WARM_PHASE_NODE_PREFERENCE, cold: COLD_PHASE_NODE_PREFERENCE, }); 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 ccdd7fcb11778..b7ca16ac46dde 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NodeDataRoleWithCatchAll } from '.'; +import { AnyDataRole } from '.'; export interface ListNodesRouteResponse { nodesByAttributes: { [attributePair: string]: string[] }; - nodesByRoles: { [role in NodeDataRoleWithCatchAll]?: string[] }; + nodesByRoles: { [role in AnyDataRole]?: string[] }; /** * A flag to indicate whether a node is using `settings.node.data` which is the now deprecated way cloud configured diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts index 1f41370e48f18..737e5a551aae8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -9,8 +9,17 @@ export * from './api'; export * from './policies'; /** - * These roles reflect how nodes are stratified into different data tiers. The "data" role - * is a catch-all that can be used to store data in any phase. + * These roles reflect how nodes are stratified into different data tiers. */ -export type NodeDataRole = 'data_hot' | 'data_warm' | 'data_cold'; -export type NodeDataRoleWithCatchAll = 'data' | NodeDataRole; +export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold'; + +/** + * The "data_content" role can store all data the ES stack uses for feature + * functionality like security-related indices. + */ +export type DataRole = 'data_content' | DataTierRole; + +/** + * The "data" role can store data allocated to any tier. + */ +export type AnyDataRole = 'data' | DataRole; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts index 6daae57330886..179de0b1d8c8e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts @@ -5,14 +5,14 @@ */ import { - NodeDataRole, + DataTierRole, ListNodesRouteResponse, PhaseWithAllocation, } from '../../../../common/types'; import { phaseToNodePreferenceMap } from '../../../../common/constants'; -export type AllocationNodeRole = NodeDataRole | 'none'; +export type AllocationNodeRole = DataTierRole | 'none'; /** * Given a phase and current cluster node roles, determine which nodes the phase diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/is_node_role_first_preference.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/is_node_role_first_preference.ts index 872efa740b131..fff640a5eb957 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/is_node_role_first_preference.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/is_node_role_first_preference.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NodeDataRole, PhaseWithAllocation } from '../../../../common/types'; +import { DataTierRole, PhaseWithAllocation } from '../../../../common/types'; import { phaseToNodePreferenceMap } from '../../../../common/constants'; -export const isNodeRoleFirstPreference = (phase: PhaseWithAllocation, nodeRole: NodeDataRole) => { +export const isNodeRoleFirstPreference = (phase: PhaseWithAllocation, nodeRole: DataTierRole) => { return phaseToNodePreferenceMap[phase][0] === nodeRole; }; 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 index 2dff55ac10de1..fc87b553ba521 100644 --- 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 @@ -5,22 +5,50 @@ */ import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { useKibana } from '../../../../../shared_imports'; + +const deployment = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body.elasticDeploymentLink', + { + defaultMessage: 'deployment', + } +); const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.coldTierTitle', { 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.', - }), + body: (deploymentUrl?: string) => { + return ( + + {deployment} + + ) : ( + deployment + ), + }} + /> + ); + }, }; export const CloudDataTierCallout: FunctionComponent = () => { + const { + services: { cloud }, + } = useKibana(); + return ( - {i18nTexts.body} + {i18nTexts.body(cloud?.cloudDeploymentUrl)} ); }; 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 42f9e8494a0b3..3d0052c69607b 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 @@ -8,11 +8,11 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiCallOut } from '@elastic/eui'; -import { PhaseWithAllocation, NodeDataRole } from '../../../../../../common/types'; +import { PhaseWithAllocation, DataTierRole } from '../../../../../../common/types'; import { AllocationNodeRole } from '../../../../lib'; -const i18nTextsNodeRoleToDataTier: Record = { +const i18nTextsNodeRoleToDataTier: Record = { data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { defaultMessage: 'hot', }), @@ -31,7 +31,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title', { defaultMessage: 'No nodes assigned to the warm tier' } ), - body: (nodeRole: NodeDataRole) => + body: (nodeRole: DataTierRole) => i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm', { defaultMessage: 'This policy will move data in the warm phase to {tier} tier nodes instead.', @@ -43,7 +43,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title', { defaultMessage: 'No nodes assigned to the cold tier' } ), - body: (nodeRole: NodeDataRole) => + body: (nodeRole: DataTierRole) => i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold', { defaultMessage: 'This policy will move data in the cold phase to {tier} tier nodes instead.', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx index de7f321e5f15d..abed1bd3a8482 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx @@ -55,26 +55,30 @@ export const DataTierAllocationField: FunctionComponent = ({ return ( {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { + const hasDataNodeRoles = Object.keys(nodesByRoles).some((nodeRole) => + // match any of the "data_" roles, including data_content. + nodeRole.trim().startsWith('data_') + ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); 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 ( - <> - - - - ); + if (isCloudEnabled && phase === 'cold') { + const isUsingNodeRolesAllocation = + !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; + const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; + + if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { + // Tell cloud users they can deploy nodes on cloud. + return ( + <> + + + + ); + } } const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); @@ -121,9 +125,9 @@ export const DataTierAllocationField: FunctionComponent = ({ phaseData={phaseData} isShowingErrors={isShowingErrors} nodes={nodesByAttributes} - disableDataTierOption={ - !!(isUsingDeprecatedDataRoleConfig && cloud?.isCloudEnabled) - } + disableDataTierOption={Boolean( + cloud?.isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig + )} /> {/* Data tier related warnings and call-to-action notices */} 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 53955d93c1e09..0603ebf6eebe0 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types'; +import { ListNodesRouteResponse, DataTierRole } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; @@ -39,10 +39,10 @@ export function convertSettingsIntoLists( } } - const dataRoles = nodeSettings.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; + const dataRoles = nodeSettings.roles.filter((r) => r.startsWith('data')) as DataTierRole[]; for (const role of dataRoles) { - accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? []; - accum.nodesByRoles[role as NodeDataRole]!.push(nodeId); + accum.nodesByRoles[role as DataTierRole] = accum.nodesByRoles[role] ?? []; + accum.nodesByRoles[role as DataTierRole]!.push(nodeId); } // If we detect a single node using legacy "data:true" setting we know we are not using data roles for diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 5c249ee474b00..6e96ef56d683f 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -361,9 +361,10 @@ export class IndexActionsContextMenu extends Component {

diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 2dcab5b49dcdb..2d84e36f3a3ac 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -26,7 +26,6 @@ import { RequestHandlerContext, KibanaResponseFactory, RouteMethod, - LegacyAPICaller, } from '../../../../../../../src/core/server'; import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; @@ -218,11 +217,7 @@ export class KibanaFramework { } public getIndexPatternsService(requestContext: RequestHandlerContext): IndexPatternsFetcher { - return new IndexPatternsFetcher((...rest: Parameters) => { - rest[1] = rest[1] || {}; - rest[1].allowNoIndices = true; - return requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(...rest); - }); + return new IndexPatternsFetcher(requestContext.core.elasticsearch.client.asCurrentUser, true); } public getSpaceId(request: KibanaRequest): string { diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index b2664d5a6d9fe..ee71167b5a1c7 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -58,7 +58,14 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); - const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); + const info = await getNodeInfo( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index d0f0bd18b5d56..f1341c7ec8101 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -20,7 +20,8 @@ export const getNodeInfo = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: InventoryItemType + nodeType: InventoryItemType, + timeRange: { from: number; to: number } ): Promise => { // If the nodeType is a Kubernetes pod then we need to get the node info // from a host record instead of a pod. This is due to the fact that any host @@ -33,7 +34,8 @@ export const getNodeInfo = async ( requestContext, sourceConfiguration, nodeId, - nodeType + nodeType, + timeRange ); if (kubernetesNodeName) { return getNodeInfo( @@ -41,12 +43,14 @@ export const getNodeInfo = async ( requestContext, sourceConfiguration, kubernetesNodeName, - 'host' + 'host', + timeRange ); } return {}; } const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const timestampField = sourceConfiguration.fields.timestamp; const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -55,9 +59,21 @@ export const getNodeInfo = async ( body: { size: 1, _source: ['host.*', 'cloud.*'], + sort: [{ [timestampField]: 'desc' }], query: { bool: { - filter: [{ match: { [fields.id]: nodeId } }], + filter: [ + { match: { [fields.id]: nodeId } }, + { + range: { + [timestampField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, + ], }, }, }, diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index be6e29a794d09..b4656178d395e 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -15,9 +15,11 @@ export const getPodNodeName = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: 'host' | 'pod' | 'container', + timeRange: { from: number; to: number } ): Promise => { const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const timestampField = sourceConfiguration.fields.timestamp; const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -26,11 +28,21 @@ export const getPodNodeName = async ( body: { size: 1, _source: ['kubernetes.node.name'], + sort: [{ [timestampField]: 'desc' }], query: { bool: { filter: [ { match: { [fields.id]: nodeId } }, { exists: { field: `kubernetes.node.name` } }, + { + range: { + [timestampField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts index aa9fbc20fc0b0..5fe72fd57f3ed 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts @@ -63,6 +63,7 @@ export interface DeleteAgentPolicyRequest { export interface DeleteAgentPolicyResponse { id: string; + name: string; } export interface GetFullAgentPolicyRequest { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts index 61669ab876d80..171902471e178 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts @@ -52,5 +52,6 @@ export interface DeletePackagePoliciesRequest { export type DeletePackagePoliciesResponse = Array<{ id: string; + name?: string; success: boolean; }>; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index d71b90d16725d..5d39995922b93 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -18,7 +18,9 @@ interface Props { } const Container = styled.div` - min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); + min-height: calc( + 100vh - ${(props) => parseFloat(props.theme.eui.euiHeaderHeightCompensation) * 2}px + ); background: ${(props) => props.theme.eui.euiColorEmptyShade}; display: flex; flex-direction: column; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx index 0266b4a90abe4..ab8a9c99227c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -63,7 +63,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle', { defaultMessage: "Deleted agent policy '{id}'", - values: { id: agentPolicy }, + values: { id: data.name || data.id }, }) ); if (onSuccessCallback.current) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx index 9242c6eb86225..ff5961b94f726 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -106,7 +106,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ 'xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle', { defaultMessage: "Deleted integration '{id}'", - values: { id: successfulResults[0].id }, + values: { id: successfulResults[0].name || successfulResults[0].id }, } ); notifications.toasts.addSuccess(successMessage); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index fa9ce24935429..af4c2f78f14a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -22,7 +22,6 @@ import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentPolicyRefresh } from '../../hooks'; interface InMemoryPackagePolicy extends PackagePolicy { - inputTypes: string[]; packageName?: string; packageTitle?: string; packageVersion?: string; @@ -56,11 +55,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ // With the package policies provided on input, generate the list of package policies // used in the InMemoryTable (flattens some values for search) as well as // the list of options that will be used in the filters dropdowns - const [packagePolicies, namespaces, inputTypes] = useMemo((): [ - InMemoryPackagePolicy[], - FilterOption[], - FilterOption[] - ] => { + const [packagePolicies, namespaces] = useMemo((): [InMemoryPackagePolicy[], FilterOption[]] => { const namespacesValues: string[] = []; const inputTypesValues: string[] = []; const mappedPackagePolicies = originalPackagePolicies.map( @@ -69,13 +64,8 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.push(packagePolicy.namespace); } - const dsInputTypes: string[] = []; - - dsInputTypes.sort(stringSortAscending); - return { ...packagePolicy, - inputTypes: dsInputTypes, packageName: packagePolicy.package?.name ?? '', packageTitle: packagePolicy.package?.title ?? '', packageVersion: packagePolicy.package?.version ?? '', @@ -86,11 +76,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.sort(stringSortAscending); inputTypesValues.sort(stringSortAscending); - return [ - mappedPackagePolicies, - namespacesValues.map(toFilterOption), - inputTypesValues.map(toFilterOption), - ]; + return [mappedPackagePolicies, namespacesValues.map(toFilterOption)]; }, [originalPackagePolicies]); const columns = useMemo( @@ -273,13 +259,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ name: 'Namespace', options: namespaces, multiSelect: 'or', - }, - { - type: 'field_value_selection', - field: 'inputTypes', - name: 'Input types', - options: inputTypes, - multiSelect: 'or', + operator: 'exact', }, ], }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index bb109d766c50a..4e32fa0bbc1b9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -182,7 +182,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { [] ); - const filterOptions: { [key: string]: string[] } = { + const filterOptions: { + [key: string]: Array<{ + value: string; + name: string; + }>; + } = { dataset: [], type: [], namespace: [], @@ -190,21 +195,37 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { }; if (dataStreamsData && dataStreamsData.data_streams.length) { + const dataValues: { + [key: string]: string[]; + } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; dataStreamsData.data_streams.forEach((stream) => { const { dataset, type, namespace, package: pkg } = stream; - if (!filterOptions.dataset.includes(dataset)) { - filterOptions.dataset.push(dataset); + if (!dataValues.dataset.includes(dataset)) { + dataValues.dataset.push(dataset); } - if (!filterOptions.type.includes(type)) { - filterOptions.type.push(type); + if (!dataValues.type.includes(type)) { + dataValues.type.push(type); } - if (!filterOptions.namespace.includes(namespace)) { - filterOptions.namespace.push(namespace); + if (!dataValues.namespace.includes(namespace)) { + dataValues.namespace.push(namespace); } - if (!filterOptions.package.includes(pkg)) { - filterOptions.package.push(pkg); + if (!dataValues.package.includes(pkg)) { + dataValues.package.push(pkg); } }); + for (const field in dataValues) { + if (filterOptions[field]) { + filterOptions[field] = dataValues[field].sort().map((option) => ({ + value: option, + name: option, + })); + } + } } return ( @@ -266,10 +287,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Dataset', }), multiSelect: 'or', - options: filterOptions.dataset.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.dataset, }, { type: 'field_value_selection', @@ -278,10 +297,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Type', }), multiSelect: 'or', - options: filterOptions.type.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.type, }, { type: 'field_value_selection', @@ -290,10 +307,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Namespace', }), multiSelect: 'or', - options: filterOptions.namespace.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.namespace, }, { type: 'field_value_selection', @@ -302,10 +317,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Integration', }), multiSelect: 'or', - options: filterOptions.package.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.package, }, ], }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 83cbb9ccb728c..bc37338f04394 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -173,6 +173,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent data states const [showInactive, setShowInactive] = useState(false); const [showUpgradeable, setShowUpgradeable] = useState(false); + // Table and search states const [search, setSearch] = useState(defaultKuery); const [selectionMode, setSelectionMode] = useState('manual'); @@ -188,11 +189,20 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); const [selectedStatus, setSelectedStatus] = useState([]); + const isUsingFilter = + search.trim() || + selectedAgentPolicies.length || + selectedStatus.length || + showInactive || + showUpgradeable; + const clearFilters = useCallback(() => { setSearch(''); setSelectedAgentPolicies([]); setSelectedStatus([]); - }, [setSearch, setSelectedAgentPolicies, setSelectedStatus]); + setShowInactive(false); + setShowUpgradeable(false); + }, [setSearch, setSelectedAgentPolicies, setSelectedStatus, setShowInactive, setShowUpgradeable]); // Add a agent policy id to current search const addAgentPolicyFilter = (policyId: string) => { @@ -638,7 +648,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { id="xpack.ingestManager.agentList.loadingAgentsMessage" defaultMessage="Loading agents…" /> - ) : search.trim() || selectedAgentPolicies.length || selectedStatus.length ? ( + ) : isUsingFilter ? ( Promise + ( + newPackagePolicy: NewPackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest + ) => Promise ]; export type ExternalCallbacksStorage = Map>; diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts index db23d6a139f20..44c2ccda3bd2a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.test.ts @@ -168,45 +168,53 @@ describe('When calling package policy', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(callbackOne).toHaveBeenCalledWith({ - policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', - description: '', - enabled: true, - inputs: [], - name: 'endpoint-1', - namespace: 'default', - output_id: '', - package: { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', + expect(callbackOne).toHaveBeenCalledWith( + { + policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, }, - }); - expect(callbackTwo).toHaveBeenCalledWith({ - policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', - description: '', - enabled: true, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - one: { - value: 'inserted by callbackOne', + context, + request + ); + expect(callbackTwo).toHaveBeenCalledWith( + { + policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, }, }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', }, - ], - name: 'endpoint-1', - namespace: 'default', - output_id: '', - package: { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', }, - }); + context, + request + ); }); it('should create with data from callback', async () => { diff --git a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts index 183488265e5af..9e582e6960ade 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_policy/handlers.ts @@ -90,7 +90,7 @@ export const createPackagePolicyHandler: RequestHandler< try { // ensure that the returned value by the callback passes schema validation updatedNewData = CreatePackagePolicyRequestSchema.body.validate( - await callback(updatedNewData) + await callback(updatedNewData, context, request) ); } catch (error) { // Log the error, but keep going and process the other callbacks 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 31bf3e7207774..b003d16d379ca 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -83,7 +83,12 @@ class AgentPolicyService { return (await this.get(soClient, id)) as AgentPolicy; } - public async ensureDefaultAgentPolicy(soClient: SavedObjectsClientContract) { + public async ensureDefaultAgentPolicy( + soClient: SavedObjectsClientContract + ): Promise<{ + created: boolean; + defaultAgentPolicy: AgentPolicy; + }> { const agentPolicies = await soClient.find({ type: AGENT_POLICY_SAVED_OBJECT_TYPE, searchFields: ['is_default'], @@ -95,12 +100,18 @@ class AgentPolicyService { ...DEFAULT_AGENT_POLICY, }; - return this.create(soClient, newDefaultAgentPolicy); + return { + created: true, + defaultAgentPolicy: await this.create(soClient, newDefaultAgentPolicy), + }; } return { - id: agentPolicies.saved_objects[0].id, - ...agentPolicies.saved_objects[0].attributes, + created: false, + defaultAgentPolicy: { + id: agentPolicies.saved_objects[0].id, + ...agentPolicies.saved_objects[0].attributes, + }, }; } @@ -404,7 +415,9 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } - const { id: defaultAgentPolicyId } = await this.ensureDefaultAgentPolicy(soClient); + const { + defaultAgentPolicy: { id: defaultAgentPolicyId }, + } = await this.ensureDefaultAgentPolicy(soClient); if (id === defaultAgentPolicyId) { throw new Error('The default agent policy cannot be deleted'); } @@ -429,6 +442,7 @@ class AgentPolicyService { await this.triggerAgentPolicyUpdatedEvent(soClient, 'deleted', id); return { id, + name: agentPolicy.name, }; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 8a038e5de2675..f2cdd1f31e69f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -247,11 +247,16 @@ export async function getNewActionsSince( nodeTypes.literal.buildNode(false), ]) ), + nodeTypes.function.buildNodeWithArgumentNodes('is', [ + nodeTypes.literal.buildNode(`${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id`), + nodeTypes.wildcard.buildNode(nodeTypes.wildcard.wildcardSymbol), + nodeTypes.literal.buildNode(false), + ]), nodeTypes.function.buildNode( 'range', `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.created_at`, { - gte: timestamp, + gt: timestamp, } ), ]); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.test.ts index f4a2147131570..04f7f206b3bcb 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.test.ts @@ -4,11 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { createAgentActionFromPolicyAction } from './state_new_actions'; -import { OutputType, Agent, AgentPolicyAction } from '../../../types'; +import { take } from 'rxjs/operators'; +import { + createAgentActionFromPolicyAction, + createNewActionsSharedObservable, +} from './state_new_actions'; +import { getNewActionsSince } from '../actions'; +import { OutputType, Agent, AgentAction, AgentPolicyAction } from '../../../types'; jest.mock('../../app_context', () => ({ appContextService: { + getInternalUserSOClient: () => { + return {}; + }, getEncryptedSavedObjects: () => ({ getDecryptedAsInternalUser: () => ({ attributes: { @@ -19,7 +27,83 @@ jest.mock('../../app_context', () => ({ }, })); +jest.mock('../actions'); + +jest.useFakeTimers(); + +function waitForPromiseResolved() { + return new Promise((resolve) => setImmediate(resolve)); +} + +function getMockedNewActionSince() { + return getNewActionsSince as jest.MockedFunction; +} + describe('test agent checkin new action services', () => { + describe('newAgetActionObservable', () => { + beforeEach(() => { + (getNewActionsSince as jest.MockedFunction).mockReset(); + }); + it('should work, call get actions until there is new action', async () => { + const observable = createNewActionsSharedObservable(); + + getMockedNewActionSince() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + ({ id: 'action1', created_at: new Date().toISOString() } as unknown) as AgentAction, + ]) + .mockResolvedValueOnce([ + ({ id: 'action2', created_at: new Date().toISOString() } as unknown) as AgentAction, + ]); + // First call + const promise = observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + const res = await promise; + expect(getNewActionsSince).toBeCalledTimes(2); + expect(res).toHaveLength(1); + expect(res[0].id).toBe('action1'); + // Second call + const secondSubscription = observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + const secondRes = await secondSubscription; + expect(secondRes).toHaveLength(1); + expect(secondRes[0].id).toBe('action2'); + expect(getNewActionsSince).toBeCalledTimes(3); + // It should call getNewActionsSince with the last action returned + expect(getMockedNewActionSince().mock.calls[2][1]).toBe(res[0].created_at); + }); + + it('should not fetch actions concurrently', async () => { + const observable = createNewActionsSharedObservable(); + + const resolves: Array<() => void> = []; + getMockedNewActionSince().mockImplementation(() => { + return new Promise((resolve) => { + resolves.push(resolve); + }); + }); + + observable.pipe(take(1)).toPromise(); + + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + jest.advanceTimersByTime(5000); + await waitForPromiseResolved(); + + expect(getNewActionsSince).toBeCalledTimes(1); + }); + }); + describe('createAgentActionFromPolicyAction()', () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); const mockAgent: Agent = { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 1871cd2cb04f6..aa48d8fe18e9f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -12,6 +12,7 @@ import { share, distinctUntilKeyChanged, switchMap, + exhaustMap, concatMap, merge, filter, @@ -62,18 +63,28 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } -function createNewActionsSharedObservable(): Observable { +export function createNewActionsSharedObservable(): Observable { let lastTimestamp = new Date().toISOString(); return timer(0, AGENT_UPDATE_ACTIONS_INTERVAL_MS).pipe( - switchMap(() => { + exhaustMap(() => { const internalSOClient = getInternalUserSOClient(); - const timestamp = lastTimestamp; - lastTimestamp = new Date().toISOString(); - return from(getNewActionsSince(internalSOClient, timestamp)); + return from( + getNewActionsSince(internalSOClient, lastTimestamp).then((data) => { + if (data.length > 0) { + lastTimestamp = data.reduce((acc, action) => { + return acc >= action.created_at ? acc : action.created_at; + }, lastTimestamp); + } + + return data; + }) + ); + }), + filter((data) => { + return data.length > 0; }), - filter((data) => data.length > 0), share() ); } diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts index d91f6e8580fc3..dc3a4495191c9 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts @@ -286,15 +286,15 @@ class PackagePolicyService { for (const id of ids) { try { - const oldPackagePolicy = await this.get(soClient, id); - if (!oldPackagePolicy) { + const packagePolicy = await this.get(soClient, id); + if (!packagePolicy) { throw new Error('Package policy not found'); } if (!options?.skipUnassignFromAgentPolicies) { await agentPolicyService.unassignPackagePolicies( soClient, - oldPackagePolicy.policy_id, - [oldPackagePolicy.id], + packagePolicy.policy_id, + [packagePolicy.id], { user: options?.user, } @@ -303,6 +303,7 @@ class PackagePolicyService { await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, + name: packagePolicy.name, success: true, }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 7f379d3ea4f13..741a23824f010 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -49,7 +49,11 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser ): Promise { - const [installedPackages, defaultOutput, defaultAgentPolicy] = await Promise.all([ + const [ + installedPackages, + defaultOutput, + { created: defaultAgentPolicyCreated, defaultAgentPolicy }, + ] = await Promise.all([ // packages installed by default ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), @@ -66,44 +70,46 @@ async function createSetupSideEffects( }), ]); - // ensure default packages are added to the default conifg - const agentPolicyWithPackagePolicies = await agentPolicyService.get( - soClient, - defaultAgentPolicy.id, - true - ); - if (!agentPolicyWithPackagePolicies) { - throw new Error('Policy not found'); - } - if ( - agentPolicyWithPackagePolicies.package_policies.length && - typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string' - ) { - throw new Error('Policy not found'); - } - - for (const installedPackage of installedPackages) { - const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some( - (packageName) => installedPackage.name === packageName + // If we just created the default policy, ensure default packages are added to it + if (defaultAgentPolicyCreated) { + const agentPolicyWithPackagePolicies = await agentPolicyService.get( + soClient, + defaultAgentPolicy.id, + true ); - if (!packageShouldBeInstalled) { - continue; + if (!agentPolicyWithPackagePolicies) { + throw new Error('Policy not found'); + } + if ( + agentPolicyWithPackagePolicies.package_policies.length && + typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string' + ) { + throw new Error('Policy not found'); } - const isInstalled = agentPolicyWithPackagePolicies.package_policies.some( - (d: PackagePolicy | string) => { - return typeof d !== 'string' && d.package?.name === installedPackage.name; + for (const installedPackage of installedPackages) { + const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some( + (packageName) => installedPackage.name === packageName + ); + if (!packageShouldBeInstalled) { + continue; } - ); - if (!isInstalled) { - await addPackageToAgentPolicy( - soClient, - callCluster, - installedPackage, - agentPolicyWithPackagePolicies, - defaultOutput + const isInstalled = agentPolicyWithPackagePolicies.package_policies.some( + (d: PackagePolicy | string) => { + return typeof d !== 'string' && d.package?.name === installedPackage.name; + } ); + + if (!isInstalled) { + await addPackageToAgentPolicy( + soClient, + callCluster, + installedPackage, + agentPolicyWithPackagePolicies, + defaultOutput + ); + } } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index d91865c21a2a6..3e05d4ddfbc20 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -28,6 +28,7 @@ import { IBasePath } from '../../../../../../src/core/public'; import { AttributeService } from '../../../../../../src/plugins/embeddable/public'; import { LensAttributeService } from '../../lens_attribute_service'; import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal'; +import { act } from 'react-dom/test-utils'; jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -337,10 +338,12 @@ describe('embeddable', () => { } as LensEmbeddableInput); embeddable.render(mountpoint); - embeddable.updateInput({ - timeRange, - query, - filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], + act(() => { + embeddable.updateInput({ + timeRange, + query, + filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], + }); }); expect(expressionRenderer).toHaveBeenCalledTimes(1); @@ -384,7 +387,9 @@ describe('embeddable', () => { } as LensEmbeddableInput); embeddable.render(mountpoint); - autoRefreshFetchSubject.next(); + act(() => { + autoRefreshFetchSubject.next(); + }); expect(expressionRenderer).toHaveBeenCalledTimes(2); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index d51b8c195c92c..3706611575c6b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -312,6 +312,37 @@ describe('xy_visualization', () => { expect(options.map((o) => o.groupId)).toEqual(['x', 'y', 'breakdown']); }); + it('should return the correct labels for the 3 dimensios', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options.map((o) => o.groupLabel)).toEqual([ + 'Horizontal axis', + 'Vertical axis', + 'Break down by', + ]); + }); + + it('should return the correct labels for the 3 dimensios for a horizontal chart', () => { + const initialState = exampleState(); + const state = { + ...initialState, + layers: [{ ...initialState.layers[0], seriesType: 'bar_horizontal' as SeriesType }], + }; + const options = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'first', + }).groups; + expect(options.map((o) => o.groupLabel)).toEqual([ + 'Vertical axis', + 'Horizontal axis', + 'Break down by', + ]); + }); + it('should only accept bucketed operations for x', () => { const options = xyVisualization.getConfiguration({ state: exampleState(), diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 76c5a51cb7168..50fbf3f01e34e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -156,13 +156,18 @@ export const xyVisualization: Visualization = { getConfiguration(props) { const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; + const isHorizontal = isHorizontalChart(props.state.layers); return { groups: [ { groupId: 'x', - groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { - defaultMessage: 'X-axis', - }), + groupLabel: isHorizontal + ? i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { + defaultMessage: 'Vertical axis', + }) + : i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { + defaultMessage: 'Horizontal axis', + }), accessors: layer.xAccessor ? [layer.xAccessor] : [], filterOperations: isBucketed, suggestedPriority: 1, @@ -172,9 +177,13 @@ export const xyVisualization: Visualization = { }, { groupId: 'y', - groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { - defaultMessage: 'Y-axis', - }), + groupLabel: isHorizontal + ? i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { + defaultMessage: 'Horizontal axis', + }) + : i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { + defaultMessage: 'Vertical axis', + }), accessors: layer.accessors, filterOperations: isNumericMetric, supportsMoreColumns: true, diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index b7a07e98437a1..57f83b9533bda 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -379,11 +379,13 @@ export function removeSelectedLayer() { ) => { const state = getState(); const layerId = getSelectedLayerId(state); - dispatch(removeLayer(layerId)); + if (layerId) { + dispatch(removeLayer(layerId)); + } }; } -export function removeLayer(layerId: string | null) { +export function removeLayer(layerId: string) { return async ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -398,7 +400,7 @@ export function removeLayer(layerId: string | null) { }; } -function removeLayerFromLayerList(layerId: string | null) { +function removeLayerFromLayerList(layerId: string) { return ( dispatch: ThunkDispatch, getState: () => MapStoreState @@ -411,7 +413,7 @@ function removeLayerFromLayerList(layerId: string | null) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(cleanTooltipStateForLayer(layerId!)); + dispatch(cleanTooltipStateForLayer(layerId)); layerGettingRemoved.destroy(); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index 58621b0a70e04..cbb6f0a4054cc 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -27,6 +27,10 @@ describe('map_actions', () => { require('../selectors/map_selectors').getDataFilters = () => { return {}; }; + + require('../selectors/map_selectors').getLayerList = () => { + return []; + }; }); it('should add newMapConstants to dispatch action mapState', async () => { diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 4efdd3dda344e..4e76bb24c9e34 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -18,6 +18,7 @@ import { getWaitingForMapReadyLayerListRaw, getQuery, getTimeFilters, + getLayerList, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -56,6 +57,7 @@ import { } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { scaleBounds } from '../../common/elasticsearch_util'; +import { cleanTooltipStateForLayer } from './tooltip_actions'; export function setMapInitError(errorMessage: string) { return { @@ -128,8 +130,7 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt dispatch: ThunkDispatch, getState: () => MapStoreState ) => { - const state = getState(); - const dataFilters = getDataFilters(state); + const dataFilters = getDataFilters(getState()); const { extent, zoom: newZoom } = newMapConstants; const { buffer, zoom: currentZoom } = dataFilters; @@ -164,6 +165,15 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt ...newMapConstants, }, }); + + if (currentZoom !== newZoom) { + getLayerList(getState()).map((layer) => { + if (!layer.showAtZoomLevel(newZoom)) { + dispatch(cleanTooltipStateForLayer(layer.getId())); + } + }); + } + await dispatch(syncDataForAllLayers()); }; } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index 12451e04efbc3..98a121e6be7a3 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -72,7 +72,7 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function cleanTooltipStateForLayer(layerId: string | null, layerFeatures: Feature[] = []) { +export function cleanTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) { return (dispatch: Dispatch, getState: () => MapStoreState) => { let featuresRemoved = false; const openTooltips = getOpenTooltips(getState()) diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx index 1f74b0d6d1449..d9b60b670b93e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx @@ -5,17 +5,21 @@ */ import { i18n } from '@kbn/i18n'; -import { getNavigateToApp } from '../../../kibana_services'; +import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services'; import { goToSpecifiedPath } from '../../maps_router'; import { getAppTitle } from '../../../../common/i18n_getters'; export const unsavedChangesWarning = i18n.translate( 'xpack.maps.breadCrumbs.unsavedChangesWarning', { - defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', + defaultMessage: 'Leave Maps with unsaved work?', } ); +export const unsavedChangesTitle = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesTitle', { + defaultMessage: 'Unsaved changes', +}); + export function getBreadcrumbs({ title, getHasUnsavedChanges, @@ -39,10 +43,13 @@ export function getBreadcrumbs({ breadcrumbs.push({ text: getAppTitle(), - onClick: () => { + onClick: async () => { if (getHasUnsavedChanges()) { - const navigateAway = window.confirm(unsavedChangesWarning); - if (navigateAway) { + const confirmed = await getCoreOverlays().openConfirm(unsavedChangesWarning, { + title: unsavedChangesTitle, + 'data-test-subj': 'appLeaveConfirmModal', + }); + if (confirmed) { goToSpecifiedPath('/'); } } else { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx index bd08b2f11fadc..df46d5d6a13ff 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx @@ -43,7 +43,7 @@ import { import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from './top_nav_config'; -import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs'; +import { getBreadcrumbs, unsavedChangesTitle, unsavedChangesWarning } from './get_breadcrumbs'; import { LayerDescriptor, MapRefreshConfig, @@ -138,9 +138,7 @@ export class MapsAppView extends React.Component { this.props.onAppLeave((actions) => { if (this._hasUnsavedChanges()) { - if (!window.confirm(unsavedChangesWarning)) { - return {} as AppLeaveAction; - } + return actions.confirm(unsavedChangesWarning, unsavedChangesTitle); } return actions.default() as AppLeaveAction; }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 7747fe5746c29..85bd5618300fe 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -18,6 +18,7 @@ import { EuiLink, EuiSelect, EuiSpacer, + EuiSwitch, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -138,7 +139,7 @@ export const AdvancedStepForm: FC = ({ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setEstimatedModelMemoryLimit, setFormState } = actions; - const { form, isJobCreated } = state; + const { form, isJobCreated, estimatedModelMemoryLimit } = state; const { computeFeatureInfluence, eta, @@ -159,6 +160,7 @@ export const AdvancedStepForm: FC = ({ outlierFraction, predictionFieldName, randomizeSeed, + useEstimatedMml, } = form; const [numTopClassesOptions, setNumTopClassesOptions] = useState([ @@ -204,7 +206,9 @@ export const AdvancedStepForm: FC = ({ if (success) { if (modelMemoryLimit !== expectedMemory) { setEstimatedModelMemoryLimit(expectedMemory); - setFormState({ modelMemoryLimit: expectedMemory }); + if (useEstimatedMml === true) { + setFormState({ modelMemoryLimit: expectedMemory }); + } } } else { // Check which field is invalid @@ -481,18 +485,35 @@ export const AdvancedStepForm: FC = ({ } )} > - setFormState({ modelMemoryLimit: e.target.value })} - isInvalid={modelMemoryLimitValidationResult !== null} - data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" - /> + <> + setFormState({ modelMemoryLimit: e.target.value })} + isInvalid={modelMemoryLimitValidationResult !== null} + data-test-subj="mlAnalyticsCreateJobWizardModelMemoryInput" + /> + + + setFormState({ + useEstimatedMml: !useEstimatedMml, + }) + } + data-test-subj="mlAnalyticsCreateJobWizardUseEstimatedMml" + /> + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index dd9ecc963840a..9c166f32f1d34 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -91,6 +91,7 @@ export const ConfigurationStepForm: FC = ({ requiredFieldsError, sourceIndex, trainingPercent, + useEstimatedMml, } = form; const toastNotifications = getToastNotifications(); @@ -164,7 +165,8 @@ export const ConfigurationStepForm: FC = ({ const debouncedGetExplainData = debounce(async () => { const jobTypeChanged = previousJobType !== jobType; - const shouldUpdateModelMemoryLimit = !firstUpdate.current || !modelMemoryLimit; + const shouldUpdateModelMemoryLimit = + (!firstUpdate.current || !modelMemoryLimit) && useEstimatedMml === true; const shouldUpdateEstimatedMml = !firstUpdate.current || !modelMemoryLimit || estimatedModelMemoryLimit === ''; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 898bd7aa183cd..533a5eb6f5c34 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -93,6 +93,7 @@ export interface State { sourceIndexFieldsCheckFailed: boolean; standardizationEnabled: undefined | string; trainingPercent: number; + useEstimatedMml: boolean; }; disabled: boolean; indexPatternsMap: SourceIndexMap; @@ -161,6 +162,7 @@ export const getInitialState = (): State => ({ sourceIndexFieldsCheckFailed: false, standardizationEnabled: 'true', trainingPercent: 80, + useEstimatedMml: true, }, jobConfig: {}, disabled: diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 99f5c3eff6984..a1e9a9b4760dd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -54,6 +54,7 @@ describe('ExplorerChart', () => { ); @@ -79,6 +80,7 @@ describe('ExplorerChart', () => { seriesConfig={config} mlSelectSeverityService={mlSelectSeverityServiceMock} tooltipService={mockTooltipService} + severity={0} /> ); @@ -111,6 +113,7 @@ describe('ExplorerChart', () => { seriesConfig={config} mlSelectSeverityService={mlSelectSeverityServiceMock} tooltipService={mockTooltipService} + severity={0} /> ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 9c04e8187cd30..ee9869a202f58 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -34,8 +34,7 @@ import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { defaultMessage: - 'This selection contains too many buckets to be displayed.' + - 'The dashboard is best viewed over a shorter time range.', + 'This selection contains too many buckets to be displayed. You should shorten the time range of the view or narrow the selection in the timeline.', }); const textViewButton = i18n.translate( 'xpack.ml.explorer.charts.openInSingleMetricViewerButtonLabel', 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 12e95e859af53..b9634f0eac359 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 @@ -571,11 +571,14 @@ 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. + // Look for the chart with the shortest bucket span as this determines + // the length of the time range that can be plotted. const midpointMs = Math.ceil((selectedEarliestMs + selectedLatestMs) / 2); + const minBucketSpanMs = Math.min.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000; const maxBucketSpanMs = Math.max.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000; const pointsToPlotFullSelection = Math.ceil( - (selectedLatestMs - selectedEarliestMs) / maxBucketSpanMs + (selectedLatestMs - selectedEarliestMs) / minBucketSpanMs ); // Optimally space points 5px apart. @@ -588,16 +591,16 @@ function calculateChartRange( const halfPoints = Math.ceil(plotPoints / 2); const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); + const boundsMin = bounds.min.valueOf(); let chartRange = { - min: Math.max(midpointMs - halfPoints * maxBucketSpanMs, bounds.min.valueOf()), - max: Math.min(midpointMs + halfPoints * maxBucketSpanMs, bounds.max.valueOf()), + min: Math.max(midpointMs - halfPoints * minBucketSpanMs, boundsMin), + max: Math.min(midpointMs + halfPoints * minBucketSpanMs, bounds.max.valueOf()), }; if (plotPoints > CHART_MAX_POINTS) { - tooManyBuckets = true; // For each series being plotted, display the record with the highest score if possible. - const maxTimeSpan = maxBucketSpanMs * CHART_MAX_POINTS; + const maxTimeSpan = minBucketSpanMs * CHART_MAX_POINTS; let minMs = recordsToPlot[0][timeFieldName]; let maxMs = recordsToPlot[0][timeFieldName]; @@ -620,14 +623,33 @@ function calculateChartRange( }); if (maxMs - minMs < maxTimeSpan) { - // Expand out to cover as much as the requested time span as possible. - minMs = Math.max(selectedEarliestMs, minMs - maxTimeSpan); - maxMs = Math.min(selectedLatestMs, maxMs + maxTimeSpan); + // Expand out before and after the span with the highest scoring anomalies, + // covering as much as the requested time span as possible. + // Work out if the high scoring region is nearer the start or end of the selected time span. + const diff = maxTimeSpan - (maxMs - minMs); + if (minMs - 0.5 * diff <= selectedEarliestMs) { + minMs = Math.max(selectedEarliestMs, minMs - 0.5 * diff); + maxMs = minMs + maxTimeSpan; + } else { + maxMs = Math.min(selectedLatestMs, maxMs + 0.5 * diff); + minMs = maxMs - maxTimeSpan; + } } chartRange = { min: minMs, max: maxMs }; } + // Elasticsearch aggregation returns points at start of bucket, + // so align the min to the length of the longest bucket. + chartRange.min = Math.floor(chartRange.min / maxBucketSpanMs) * maxBucketSpanMs; + if (chartRange.min < boundsMin) { + chartRange.min = chartRange.min + maxBucketSpanMs; + } + + if (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) { + tooManyBuckets = true; + } + return { chartRange, tooManyBuckets, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss index 8d4f6ad0a6844..45678f6e71c2e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss @@ -24,10 +24,6 @@ } } - .single-metric-request-callout { - margin: 0 $euiSize; - } - .results-container { .panel-title { font-size: $euiFontSizeM; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 9d2c49a95fec4..e1323019d61db 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -9,13 +9,7 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - EuiComboBox, - EuiComboBoxOptionOption, - EuiFlexItem, - EuiFormRow, - EuiToolTip, -} from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui'; export interface Entity { fieldName: string; @@ -156,9 +150,7 @@ export class EntityControl extends Component - - {control} - + {control} ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 7d173e161a1cb..9b8764d3f9279 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1133,23 +1133,25 @@ export class TimeSeriesExplorer extends React.Component { return ( {fieldNamesWithEmptyValues.length > 0 && ( - - } - color="warning" - iconType="help" - size="s" - /> + <> + + } + color="warning" + iconType="help" + size="s" + /> + + )}
diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js index d0ca1bc6bbde6..ef97d78b4f745 100644 --- a/x-pack/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -25,7 +25,7 @@ let once = false; let inTransit = false; export function monitoringClustersProvider($injector) { - return (clusterUuid, ccs, codePaths) => { + return async (clusterUuid, ccs, codePaths) => { const { min, max } = Legacy.shims.timefilter.getBounds(); // append clusterUuid if the parameter is given @@ -36,74 +36,73 @@ export function monitoringClustersProvider($injector) { const $http = $injector.get('$http'); - function getClusters() { - return $http - .post(url, { + async function getClusters() { + try { + const response = await $http.post(url, { ccs, timeRange: { min: min.toISOString(), max: max.toISOString(), }, codePaths, - }) - .then((response) => response.data) - .then((data) => { - return formatClusters(data); // return set of clusters - }) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); }); + return formatClusters(response.data); + } catch (err) { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + } } - function ensureAlertsEnabled() { - return $http.post('../api/monitoring/v1/alerts/enable', {}).catch((err) => { + async function ensureAlertsEnabled() { + try { + return $http.post('../api/monitoring/v1/alerts/enable', {}); + } catch (err) { const Private = $injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); - }); + } } - function ensureMetricbeatEnabled() { + async function ensureMetricbeatEnabled() { if (Legacy.shims.isCloud) { - return Promise.resolve(); + return; } const globalState = $injector.get('globalState'); - return $http - .post('../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', { - ccs: globalState.ccs, - }) - .then(({ data }) => { - showInternalMonitoringToast({ - legacyIndices: data.legacy_indices, - metricbeatIndices: data.mb_indices, - }); - }) - .catch((err) => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); + try { + const response = await $http.post( + '../api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', + { + ccs: globalState.ccs, + } + ); + const { data } = response; + showInternalMonitoringToast({ + legacyIndices: data.legacy_indices, + metricbeatIndices: data.mb_indices, }); + } catch (err) { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + } } if (!once && !inTransit) { inTransit = true; - return getClusters().then((clusters) => { - if (clusters.length) { - Promise.all([ensureAlertsEnabled(), ensureMetricbeatEnabled()]) - .then(([{ data }]) => { - showSecurityToast(data); - once = true; - }) - .catch(() => { - // Intentionally swallow the error as this will retry the next page load - }) - .finally(() => (inTransit = false)); + const clusters = await getClusters(); + if (clusters.length) { + try { + const [{ data }] = await Promise.all([ensureAlertsEnabled(), ensureMetricbeatEnabled()]); + showSecurityToast(data); + once = true; + } catch (_err) { + // Intentionally swallow the error as this will retry the next page load } - return clusters; - }); + inTransit = false; + } + return clusters; } - return getClusters(); + return await getClusters(); }; } diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index 4341ada00da8b..e69d572f9560b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -63,7 +63,7 @@ uiRoutes.when('/elasticsearch/nodes', { const promise = globalState.cluster_uuid ? getNodes() - : new Promise((resolve) => resolve({})); + : new Promise((resolve) => resolve({ data: {} })); return promise .then((response) => response.data) .catch((err) => { diff --git a/x-pack/plugins/monitoring/public/views/loading/index.js b/x-pack/plugins/monitoring/public/views/loading/index.js index 5ca523899067d..b5d79e1d2b652 100644 --- a/x-pack/plugins/monitoring/public/views/loading/index.js +++ b/x-pack/plugins/monitoring/public/views/loading/index.js @@ -53,15 +53,18 @@ uiRoutes.when('/loading', { (clusters) => { if (!clusters || !clusters.length) { window.location.hash = '#/no-data'; + $scope.$apply(); return; } if (clusters.length === 1) { // Bypass the cluster listing if there is just 1 cluster window.history.replaceState(null, null, '#/overview'); + $scope.$apply(); return; } window.history.replaceState(null, null, '#/home'); + $scope.$apply(); } ); } diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts index e3d69820ebb05..5605641992e1a 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts @@ -36,7 +36,7 @@ describe('DiskUsageAlert', () => { expect(alert.type).toBe(ALERT_DISK_USAGE); expect(alert.label).toBe('Disk Usage'); expect(alert.defaultThrottle).toBe('1d'); - expect(alert.defaultParams).toStrictEqual({ threshold: 90, duration: '5m' }); + expect(alert.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); expect(alert.actionVariables).toStrictEqual([ { name: 'nodes', description: 'The list of nodes reporting high disk usage.' }, { name: 'count', description: 'The number of nodes reporting high disk usage.' }, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index c577550de8617..34c640de79625 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -54,7 +54,7 @@ export class DiskUsageAlert extends BaseAlert { public label = DiskUsageAlert.LABEL; protected defaultParams = { - threshold: 90, + threshold: 80, duration: '5m', }; diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 6ed237a055b5c..57d01dc6a1100 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -22,9 +22,9 @@ describe('MissingMonitoringDataAlert', () => { const alert = new MissingMonitoringDataAlert(); expect(alert.type).toBe(ALERT_MISSING_MONITORING_DATA); expect(alert.label).toBe('Missing monitoring data'); - expect(alert.defaultThrottle).toBe('1d'); + expect(alert.defaultThrottle).toBe('6h'); // @ts-ignore - expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' }); + expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); // @ts-ignore expect(alert.actionVariables).toStrictEqual([ { name: 'stackProducts', description: 'The stack products missing monitoring data.' }, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 252d005f1e4a6..5b4542a4439ca 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -50,7 +50,7 @@ const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', { defaultMessage: 'firing', }); -const DEFAULT_DURATION = '5m'; +const DEFAULT_DURATION = '15m'; const DEFAULT_LIMIT = '1d'; // Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back @@ -77,6 +77,8 @@ export class MissingMonitoringDataAlert extends BaseAlert { } as CommonAlertParamDetail, }; + public defaultThrottle: string = '6h'; + public type = ALERT_MISSING_MONITORING_DATA; public label = i18n.translate('xpack.monitoring.alerts.missingData.label', { defaultMessage: 'Missing monitoring data', diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index a3ff4b952ce97..0dd5ce291f972 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Collector } from '../../../../../../src/plugins/usage_collection/server'; +import { Collector, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_SETTINGS_TYPE } from '../../../common/constants'; import { MonitoringConfig } from '../../config'; @@ -48,10 +48,18 @@ export interface KibanaSettingsCollector extends Collector true, + schema: { + xpack: { + default_admin_email: { type: 'text' }, + }, + }, async fetch(this: KibanaSettingsCollector) { let kibanaSettingsData; const defaultAdminEmail = await checkForEmailValue(config); diff --git a/x-pack/plugins/painless_lab/public/application/components/editor.tsx b/x-pack/plugins/painless_lab/public/application/components/editor.tsx index b8891ce6524f5..5971c0de5c4ef 100644 --- a/x-pack/plugins/painless_lab/public/application/components/editor.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/editor.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { PainlessLang } from '@kbn/monaco'; import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; interface Props { @@ -14,7 +15,7 @@ interface Props { export function Editor({ code, onChange }: Props) { return ( , so we define our own - brackets: [ - ['{', '}', 'delimiter.curly'], - ['[', ']', 'delimiter.square'], - ['(', ')', 'delimiter.parenthesis'], - ], - keywords: [ - 'if', - 'in', - 'else', - 'while', - 'do', - 'for', - 'continue', - 'break', - 'return', - 'new', - 'try', - 'catch', - 'throw', - 'this', - 'instanceof', - ], - primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double', 'def'], - constants: ['true', 'false'], - operators: [ - '=', - '>', - '<', - '!', - '~', - '?', - '?:', - '?.', - ':', - '==', - '===', - '<=', - '>=', - '!=', - '!==', - '&&', - '||', - '++', - '--', - '+', - '-', - '*', - '/', - '&', - '|', - '^', - '%', - '<<', - '>>', - '>>>', - '+=', - '-=', - '*=', - '/=', - '&=', - '|=', - '^=', - '%=', - '<<=', - '>>=', - '>>>=', - '->', - '::', - '=~', - '==~', - ], - symbols: /[=> { @@ -25,8 +24,6 @@ const checkLicenseStatus = (license: ILicense) => { }; export class PainlessLabUIPlugin implements Plugin { - languageService = new LanguageService(); - public setup( { http, getStartServices, uiSettings }: CoreSetup, { devTools, home, licensing }: PluginDependencies @@ -80,8 +77,6 @@ export class PainlessLabUIPlugin implements Plugin { - const blob = new Blob([workerSrc], { type: 'application/javascript' }); - return new Worker(window.URL.createObjectURL(blob)); - }, - }; - } - } - - public stop() { - if (CAN_CREATE_WORKER) { - (window as any).MonacoEnvironment = this.originalMonacoEnvironment; - } - } -} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 2cf5b69835d1f..39f597acec6ec 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -12,8 +12,7 @@ import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; import { ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; -// @ts-ignore untyped module -import { pdf } from './pdf'; +import { PdfMaker } from './pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { @@ -58,7 +57,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.endScreenshots(); tracker.startSetup(); - const pdfOutput = pdf.create(layout, logo); + const pdfOutput = new PdfMaker(layout, logo); if (title) { const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange}` : ''; 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 9f7e9310333ba..fe687c3f47327 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 @@ -31,7 +31,7 @@ test(`gets logo from uiSettings`, async () => { }; const mockGet = jest.fn(); - mockGet.mockImplementationOnce((...args: any[]) => { + mockGet.mockImplementationOnce((...args: string[]) => { if (args[0] === 'xpackReporting:customPdfLogo') { return 'purple pony'; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts new file mode 100644 index 0000000000000..5a3671835ce51 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BufferOptions } from 'pdfmake/interfaces'; + +export function getDocOptions(tableBorderWidth: number): BufferOptions { + return { + tableLayouts: { + noBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + simpleBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => tableBorderWidth, + vLineWidth: () => tableBorderWidth, + hLineColor: () => 'silver', + vLineColor: () => 'silver', + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts new file mode 100644 index 0000000000000..5cd6136153f04 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore: no module definition +import xRegExp from 'xregexp'; + +export function getFont(text: string) { + // Once unicode regex scripts are fully supported we should be able to get rid of the dependency + // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes + // for more information. We are matching Han characters which is one of the supported unicode scripts + // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). + // This will match Chinese, Japanese, Korean and some other Asian languages. + const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); + if (isCKJ) { + return 'noto-cjk'; + } else { + return 'Roboto'; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts new file mode 100644 index 0000000000000..131d289576384 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 path from 'path'; +import { TDocumentDefinitions } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getFont } from './get_font'; + +export function getTemplate( + layout: LayoutInstance, + logo: string | undefined, + title: string, + tableBorderWidth: number, + assetPath: string +): Partial { + const pageMarginTop = 40; + const pageMarginBottom = 80; + const pageMarginWidth = 40; + const headingFontSize = 14; + const headingMarginTop = 10; + const headingMarginBottom = 5; + const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; + const subheadingFontSize = 12; + const subheadingMarginTop = 0; + const subheadingMarginBottom = 5; + const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; + + return { + // define page size + pageOrientation: layout.getPdfPageOrientation(), + pageSize: layout.getPdfPageSize({ + pageMarginTop, + pageMarginBottom, + pageMarginWidth, + tableBorderWidth, + headingHeight, + subheadingHeight, + }), + pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], + + header() { + return { + margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], + text: title, + font: getFont(title), + style: { + color: '#aaa', + }, + fontSize: 10, + alignment: 'center', + }; + }, + + footer(currentPage: number, pageCount: number) { + const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo + return { + margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], + layout: 'noBorder', + table: { + widths: [100, '*', 100], + body: [ + [ + { + fit: [100, 35], + image: logo || logoPath, + }, + { + alignment: 'center', + text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { + defaultMessage: 'Page {currentPage} of {pageCount}', + values: { currentPage: currentPage.toString(), pageCount }, + }), + style: { + color: '#aaa', + }, + }, + '', + ], + [ + logo + ? { + text: i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.logoDescription', + { + defaultMessage: 'Powered by Elastic', + } + ), + fontSize: 10, + style: { + color: '#aaa', + }, + margin: [0, 2, 0, 0], + } + : '', + '', + '', + ], + ], + }, + }; + }, + + styles: { + heading: { + alignment: 'left', + fontSize: headingFontSize, + bold: true, + margin: [headingMarginTop, 0, headingMarginBottom, 0], + }, + subheading: { + alignment: 'left', + fontSize: subheadingFontSize, + italics: true, + margin: [0, 0, subheadingMarginBottom, 20], + }, + warning: { + color: '#f39c12', // same as @brand-warning in Kibana colors.less + }, + }, + + defaultStyle: { + fontSize: 12, + font: 'Roboto', + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js deleted file mode 100644 index 8840fd524f3e4..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ /dev/null @@ -1,319 +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 path from 'path'; -import _ from 'lodash'; -import concat from 'concat-stream'; -import Printer from 'pdfmake'; -import xRegExp from 'xregexp'; -import { i18n } from '@kbn/i18n'; - -const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); - -const tableBorderWidth = 1; - -function getFont(text) { - // Once unicode regex scripts are fully supported we should be able to get rid of the dependency - // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes - // for more information. We are matching Han characters which is one of the supported unicode scripts - // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). - // This will match Chinese, Japanese, Korean and some other Asian languages. - const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); - if (isCKJ) { - return 'noto-cjk'; - } else { - return 'Roboto'; - } -} - -class PdfMaker { - constructor(layout, logo) { - const fontPath = (filename) => path.resolve(assetPath, 'fonts', filename); - const fonts = { - Roboto: { - normal: fontPath('roboto/Roboto-Regular.ttf'), - bold: fontPath('roboto/Roboto-Medium.ttf'), - italics: fontPath('roboto/Roboto-Italic.ttf'), - bolditalics: fontPath('roboto/Roboto-Italic.ttf'), - }, - 'noto-cjk': { - // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. - normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - }, - }; - - this._layout = layout; - this._logo = logo; - this._title = ''; - this._content = []; - this._printer = new Printer(fonts); - } - - _addContents(contents) { - const groupCount = this._content.length; - - // inject a page break for every 2 groups on the page - if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { - contents = [ - { - text: '', - pageBreak: 'after', - }, - ].concat(contents); - } - this._content.push(contents); - } - - addImage(base64EncodedData, { title = '', description = '' }) { - const contents = []; - - if (title && title.length > 0) { - contents.push({ - text: title, - style: 'heading', - font: getFont(title), - noWrap: true, - }); - } - - if (description && description.length > 0) { - contents.push({ - text: description, - style: 'subheading', - font: getFont(description), - noWrap: true, - }); - } - - const img = { - image: `data:image/png;base64,${base64EncodedData}`, - alignment: 'center', - }; - - const size = this._layout.getPdfImageSize(); - img.height = size.height; - img.width = size.width; - - const wrappedImg = { - table: { - body: [[img]], - }, - layout: 'noBorder', - }; - - contents.push(wrappedImg); - - this._addContents(contents); - } - - addHeading(headingText, opts = {}) { - const contents = []; - contents.push({ - text: headingText, - style: ['heading'].concat(opts.styles || []), - font: getFont(headingText), - }); - this._addContents(contents); - } - - setTitle(title) { - this._title = title; - } - - generate() { - const docTemplate = _.assign(getTemplate(this._layout, this._logo, this._title), { - content: this._content, - }); - this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions()); - return this; - } - - getBuffer() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - return new Promise((resolve, reject) => { - const concatStream = concat(function (pdfBuffer) { - resolve(pdfBuffer); - }); - - this._pdfDoc.on('error', reject); - this._pdfDoc.pipe(concatStream); - this._pdfDoc.end(); - }); - } - - getStream() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - this._pdfDoc.end(); - return this._pdfDoc; - } -} - -function getTemplate(layout, logo, title) { - const pageMarginTop = 40; - const pageMarginBottom = 80; - const pageMarginWidth = 40; - const headingFontSize = 14; - const headingMarginTop = 10; - const headingMarginBottom = 5; - const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; - const subheadingFontSize = 12; - const subheadingMarginTop = 0; - const subheadingMarginBottom = 5; - const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; - - return { - // define page size - pageOrientation: layout.getPdfPageOrientation(), - pageSize: layout.getPdfPageSize({ - pageMarginTop, - pageMarginBottom, - pageMarginWidth, - tableBorderWidth, - headingHeight, - subheadingHeight, - }), - pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], - - header: function () { - return { - margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], - text: title, - font: getFont(title), - style: { - color: '#aaa', - }, - fontSize: 10, - alignment: 'center', - }; - }, - - footer: function (currentPage, pageCount) { - const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); - return { - margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], - layout: 'noBorder', - table: { - widths: [100, '*', 100], - body: [ - [ - { - fit: [100, 35], - image: logo || logoPath, - }, - { - alignment: 'center', - text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { - defaultMessage: 'Page {currentPage} of {pageCount}', - values: { currentPage: currentPage.toString(), pageCount }, - }), - style: { - color: '#aaa', - }, - }, - '', - ], - [ - logo - ? { - text: i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.logoDescription', - { - defaultMessage: 'Powered by Elastic', - } - ), - fontSize: 10, - style: { - color: '#aaa', - }, - margin: [0, 2, 0, 0], - } - : '', - '', - '', - ], - ], - }, - }; - }, - - styles: { - heading: { - alignment: 'left', - fontSize: headingFontSize, - bold: true, - marginTop: headingMarginTop, - marginBottom: headingMarginBottom, - }, - subheading: { - alignment: 'left', - fontSize: subheadingFontSize, - italics: true, - marginLeft: 20, - marginBottom: subheadingMarginBottom, - }, - warning: { - color: '#f39c12', // same as @brand-warning in Kibana colors.less - }, - }, - - defaultStyle: { - fontSize: 12, - font: 'Roboto', - }, - }; -} - -function getDocOptions() { - return { - tableLayouts: { - noBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => 0, - vLineWidth: () => 0, - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - simpleBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => tableBorderWidth, - vLineWidth: () => tableBorderWidth, - hLineColor: () => 'silver', - vLineColor: () => 'silver', - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - }, - }; -} - -export const pdf = { - create: (layout, logo) => new PdfMaker(layout, logo), -}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts new file mode 100644 index 0000000000000..5c6a7c7c63c69 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PreserveLayout, PrintLayout } from '../../../../lib/layouts'; +import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers'; +import { PdfMaker } from './'; + +const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`; + +describe('PdfMaker', () => { + it('makes PDF using PrintLayout mode', async () => { + const config = createMockConfig(createMockConfigSchema()); + const layout = new PrintLayout(config.get('capture')); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the best PDF in the world')).toBe(undefined); + expect([ + pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }), + pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }), + ]).toEqual([undefined, undefined]); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data? + groupCount: 2, + id: 'print', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-item]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the best PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); + + it('makes PDF using PreserveLayout mode', async () => { + const layout = new PreserveLayout({ width: 400, height: 300 }); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined); + expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + groupCount: 1, + id: 'preserve_layout', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-items-container]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the finest PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts new file mode 100644 index 0000000000000..c58ceae85657b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// @ts-ignore: no module definition +import concat from 'concat-stream'; +import _ from 'lodash'; +import path from 'path'; +import Printer from 'pdfmake'; +import { Content, ContentText } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getDocOptions } from './get_doc_options'; +import { getFont } from './get_font'; +import { getTemplate } from './get_template'; + +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); +const tableBorderWidth = 1; + +export class PdfMaker { + private _layout: LayoutInstance; + private _logo: string | undefined; + private _title: string; + private _content: Content[]; + private _printer: Printer; + private _pdfDoc: PDFKit.PDFDocument | undefined; + + constructor(layout: LayoutInstance, logo: string | undefined) { + const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); + const fonts = { + Roboto: { + normal: fontPath('roboto/Roboto-Regular.ttf'), + bold: fontPath('roboto/Roboto-Medium.ttf'), + italics: fontPath('roboto/Roboto-Italic.ttf'), + bolditalics: fontPath('roboto/Roboto-Italic.ttf'), + }, + 'noto-cjk': { + // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. + normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + }, + }; + + this._layout = layout; + this._logo = logo; + this._title = ''; + this._content = []; + this._printer = new Printer(fonts); + } + + _addContents(contents: Content[]) { + const groupCount = this._content.length; + + // inject a page break for every 2 groups on the page + if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { + contents = [ + ({ + text: '', + pageBreak: 'after', + } as ContentText) as Content, + ].concat(contents); + } + this._content.push(contents); + } + + addImage(base64EncodedData: string, { title = '', description = '' }) { + const contents: Content[] = []; + + if (title && title.length > 0) { + contents.push({ + text: title, + style: 'heading', + font: getFont(title), + noWrap: true, + }); + } + + if (description && description.length > 0) { + contents.push({ + text: description, + style: 'subheading', + font: getFont(description), + noWrap: true, + }); + } + + const size = this._layout.getPdfImageSize(); + const img = { + image: `data:image/png;base64,${base64EncodedData}`, + alignment: 'center', + height: size.height, + width: size.width, + }; + + const wrappedImg = { + table: { + body: [[img]], + }, + layout: 'noBorder', + }; + + contents.push(wrappedImg); + + this._addContents(contents); + } + + setTitle(title: string) { + this._title = title; + } + + generate() { + const docTemplate = _.assign( + getTemplate(this._layout, this._logo, this._title, tableBorderWidth, assetPath), + { + content: this._content, + } + ); + this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions(tableBorderWidth)); + return this; + } + + getBuffer(): Promise { + return new Promise((resolve, reject) => { + if (!this._pdfDoc) { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', + { + defaultMessage: 'Document stream has not been generated', + } + ) + ); + } + + const concatStream = concat(function (pdfBuffer: Buffer) { + resolve(pdfBuffer); + }); + + this._pdfDoc.on('error', reject); + this._pdfDoc.pipe(concatStream); + this._pdfDoc.end(); + }); + } +} diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 585175aac82c5..e69b8d61dec0d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -5,11 +5,14 @@ */ import { CaptureConfig } from '../../types'; -import { LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams, LayoutTypes } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; -export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams) { +export function createLayout( + captureConfig: CaptureConfig, + layoutParams?: LayoutParams +): LayoutInstance { if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts index 433edb35df4cd..4dd4003c269c0 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/layout.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; import { PageSizeParams, PdfImageSize, Size } from './'; export interface ViewZoomWidthHeight { @@ -14,6 +15,7 @@ export interface ViewZoomWidthHeight { export abstract class Layout { public id: string = ''; + public groupCount: number = 0; constructor(id: string) { this.id = id; @@ -21,9 +23,11 @@ export abstract class Layout { public abstract getPdfImageSize(): PdfImageSize; - public abstract getPdfPageOrientation(): string | undefined; + public abstract getPdfPageOrientation(): 'portrait' | 'landscape' | undefined; - public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; + public abstract getPdfPageSize( + pageSizeParams: PageSizeParams + ): CustomPageSize | PredefinedPageSize; public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index cecd761fbcf32..faddaae64ce5d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -5,14 +5,15 @@ */ import path from 'path'; +import { CustomPageSize } from 'pdfmake/interfaces'; import { getDefaultLayoutSelectors, Layout, + LayoutInstance, LayoutSelectorDictionary, LayoutTypes, PageSizeParams, Size, - LayoutInstance, } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. @@ -72,7 +73,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { return undefined; } - public getPdfPageSize(pageSizeParams: PageSizeParams) { + public getPdfPageSize(pageSizeParams: PageSizeParams): CustomPageSize { return { height: this.height + diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 33f16bc7865d5..e979cdeeb71fe 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -90,11 +91,11 @@ export class PrintLayout extends Layout implements LayoutInstance { }; } - public getPdfPageOrientation() { + public getPdfPageOrientation(): PageOrientation { return 'portrait'; } - public getPdfPageSize() { + public getPdfPageSize(): PredefinedPageSize { return 'A4'; } } diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts index 250947d72c5fa..df9907fbf731a 100644 --- a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -8,6 +8,7 @@ import { keyBy } from 'lodash'; import { schema } from '@kbn/config-schema'; import { Field } from '../../../lib/merge_capabilities_with_fields'; import { RouteDependencies } from '../../../types'; +import type { IndexPatternsFetcher as IndexPatternsFetcherType } from '../../../../../../../src/plugins/data/server'; const parseMetaFields = (metaFields: string | string[]) => { let parsedFields: string[] = []; @@ -23,10 +24,10 @@ const getFieldsForWildcardRequest = async ( context: any, request: any, response: any, - IndexPatternsFetcher: any + IndexPatternsFetcher: typeof IndexPatternsFetcherType ) => { - const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 4c9e3bc06037e..c3fc6bd1ae1dd 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/detections/mitre/mitre_tactics_techniques.ts --fix", "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 05000f91f094c..2be9d27b3fecb 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -278,6 +278,7 @@ export const replaceStateInLocation = ({ replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) ); if (history) { + newLocation.state = history.location.state; history.replace(newLocation); } return newLocation.search; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index 589436b945a65..febcf0aee679d 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal'; import { SpyRouteProps } from './types'; import { useRouteSpy } from './use_route_spy'; +import { SecurityPageName } from '../../../../common/constants'; export const SpyRouteComponent = memo< SpyRouteProps & { location: H.Location; pageName: string | undefined } @@ -50,6 +51,7 @@ export const SpyRouteComponent = memo< pathName: pathname, state, tabName, + ...(pageName === SecurityPageName.administration ? { search: search ?? '' } : {}), }, }); setIsInitializing(false); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 9b15007136b2e..e87303efbe526 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -42,7 +42,7 @@ describe('useUserInfo', () => { isSignalIndexExists: null, loading: true, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, }, error: undefined, }); @@ -53,7 +53,7 @@ describe('useUserInfo', () => { const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex'); const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex').mockResolvedValueOnce({ name: 'mock-signal-index', - template_outdated: true, + index_mapping_outdated: true, }); await act(async () => { const { waitForNextUpdate } = renderHook(() => useUserInfo(), { wrapper: ManageUserInfo }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index ac2bf438d7fa6..3b0976f459324 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -20,7 +20,7 @@ export interface State { hasEncryptionKey: boolean | null; loading: boolean; signalIndexName: string | null; - signalIndexTemplateOutdated: boolean | null; + signalIndexMappingOutdated: boolean | null; } export const initialState: State = { @@ -32,7 +32,7 @@ export const initialState: State = { hasEncryptionKey: null, loading: true, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, }; export type Action = @@ -66,8 +66,8 @@ export type Action = signalIndexName: string | null; } | { - type: 'updateSignalIndexTemplateOutdated'; - signalIndexTemplateOutdated: boolean | null; + type: 'updateSignalIndexMappingOutdated'; + signalIndexMappingOutdated: boolean | null; }; export const userInfoReducer = (state: State, action: Action): State => { @@ -120,10 +120,10 @@ export const userInfoReducer = (state: State, action: Action): State => { signalIndexName: action.signalIndexName, }; } - case 'updateSignalIndexTemplateOutdated': { + case 'updateSignalIndexMappingOutdated': { return { ...state, - signalIndexTemplateOutdated: action.signalIndexTemplateOutdated, + signalIndexMappingOutdated: action.signalIndexMappingOutdated, }; } default: @@ -156,7 +156,7 @@ export const useUserInfo = (): State => { hasEncryptionKey, loading, signalIndexName, - signalIndexTemplateOutdated, + signalIndexMappingOutdated, }, dispatch, ] = useUserData(); @@ -171,7 +171,7 @@ export const useUserInfo = (): State => { loading: indexNameLoading, signalIndexExists: isApiSignalIndexExists, signalIndexName: apiSignalIndexName, - signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated, + signalIndexMappingOutdated: apiSignalIndexMappingOutdated, createDeSignalIndex: createSignalIndex, } = useSignalIndex(); @@ -234,15 +234,15 @@ export const useUserInfo = (): State => { useEffect(() => { if ( !loading && - signalIndexTemplateOutdated !== apiSignalIndexTemplateOutdated && - apiSignalIndexTemplateOutdated != null + signalIndexMappingOutdated !== apiSignalIndexMappingOutdated && + apiSignalIndexMappingOutdated != null ) { dispatch({ - type: 'updateSignalIndexTemplateOutdated', - signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated, + type: 'updateSignalIndexMappingOutdated', + signalIndexMappingOutdated: apiSignalIndexMappingOutdated, }); } - }, [dispatch, loading, signalIndexTemplateOutdated, apiSignalIndexTemplateOutdated]); + }, [dispatch, loading, signalIndexMappingOutdated, apiSignalIndexMappingOutdated]); useEffect(() => { if ( @@ -250,7 +250,7 @@ export const useUserInfo = (): State => { hasEncryptionKey && hasIndexManage && ((isSignalIndexExists != null && !isSignalIndexExists) || - (signalIndexTemplateOutdated != null && signalIndexTemplateOutdated)) && + (signalIndexMappingOutdated != null && signalIndexMappingOutdated)) && createSignalIndex != null ) { createSignalIndex(); @@ -261,7 +261,7 @@ export const useUserInfo = (): State => { hasEncryptionKey, isSignalIndexExists, hasIndexManage, - signalIndexTemplateOutdated, + signalIndexMappingOutdated, ]); return { @@ -273,6 +273,6 @@ export const useUserInfo = (): State => { hasIndexManage, hasIndexWrite, signalIndexName, - signalIndexTemplateOutdated, + signalIndexMappingOutdated, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index 4fd240348f0f3..21b561ec9cddb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -980,7 +980,7 @@ export const mockStatusAlertQuery: object = { export const mockSignalIndex: AlertsIndex = { name: 'mock-signal-index', - template_outdated: false, + index_mapping_outdated: false, }; export const mockUserPrivilege: Privilege = { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index 59ab416ecc824..dadeb1e7958b5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -44,7 +44,7 @@ export interface UpdateAlertStatusProps { export interface AlertsIndex { name: string; - template_outdated: boolean; + index_mapping_outdated: boolean; } export interface Privilege { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index 1db952526414a..07375a31f3bbc 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -26,7 +26,7 @@ describe('useSignalIndex', () => { loading: true, signalIndexExists: null, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, }); }); }); @@ -43,7 +43,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: true, signalIndexName: 'mock-signal-index', - signalIndexTemplateOutdated: false, + signalIndexMappingOutdated: false, }); }); }); @@ -64,7 +64,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: true, signalIndexName: 'mock-signal-index', - signalIndexTemplateOutdated: false, + signalIndexMappingOutdated: false, }); }); }); @@ -104,7 +104,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: false, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, }); }); }); @@ -125,7 +125,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: false, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index f7d2202736169..1233456359b7f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -17,7 +17,7 @@ export interface ReturnSignalIndex { loading: boolean; signalIndexExists: boolean | null; signalIndexName: string | null; - signalIndexTemplateOutdated: boolean | null; + signalIndexMappingOutdated: boolean | null; createDeSignalIndex: Func | null; } @@ -31,7 +31,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { const [signalIndex, setSignalIndex] = useState>({ signalIndexExists: null, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, createDeSignalIndex: null, }); const [, dispatchToaster] = useStateToaster(); @@ -49,7 +49,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: true, signalIndexName: signal.name, - signalIndexTemplateOutdated: signal.template_outdated, + signalIndexMappingOutdated: signal.index_mapping_outdated, createDeSignalIndex: createIndex, }); } @@ -58,7 +58,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: false, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, createDeSignalIndex: createIndex, }); if (isSecurityAppError(error) && error.body.status_code !== 404) { @@ -89,7 +89,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: false, signalIndexName: null, - signalIndexTemplateOutdated: null, + signalIndexMappingOutdated: null, createDeSignalIndex: createIndex, }); errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster }); diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index fb8deeec8309c..027aa7fd699e4 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -78,9 +78,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0009', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.collectionDescription', - { - defaultMessage: 'Collection (TA0009)', - } + { defaultMessage: 'Collection (TA0009)' } ), value: 'collection', }, @@ -120,9 +118,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0007', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.discoveryDescription', - { - defaultMessage: 'Discovery (TA0007)', - } + { defaultMessage: 'Discovery (TA0007)' } ), value: 'discovery', }, @@ -132,9 +128,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0002', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.executionDescription', - { - defaultMessage: 'Execution (TA0002)', - } + { defaultMessage: 'Execution (TA0002)' } ), value: 'execution', }, @@ -144,9 +138,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0010', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.exfiltrationDescription', - { - defaultMessage: 'Exfiltration (TA0010)', - } + { defaultMessage: 'Exfiltration (TA0010)' } ), value: 'exfiltration', }, @@ -156,9 +148,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0040', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.impactDescription', - { - defaultMessage: 'Impact (TA0040)', - } + { defaultMessage: 'Impact (TA0040)' } ), value: 'impact', }, @@ -168,9 +158,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0001', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.initialAccessDescription', - { - defaultMessage: 'Initial Access (TA0001)', - } + { defaultMessage: 'Initial Access (TA0001)' } ), value: 'initialAccess', }, @@ -190,9 +178,7 @@ export const tacticsOptions: MitreTacticsOptions[] = [ reference: 'https://attack.mitre.org/tactics/TA0003', text: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTactics.persistenceDescription', - { - defaultMessage: 'Persistence (TA0003)', - } + { defaultMessage: 'Persistence (TA0003)' } ), value: 'persistence', }, @@ -1998,9 +1984,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bitsJobsDescription', - { - defaultMessage: 'BITS Jobs (T1197)', - } + { defaultMessage: 'BITS Jobs (T1197)' } ), id: 'T1197', name: 'BITS Jobs', @@ -2033,9 +2017,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription', - { - defaultMessage: 'Bootkit (T1067)', - } + { defaultMessage: 'Bootkit (T1067)' } ), id: 'T1067', name: 'Bootkit', @@ -2090,9 +2072,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription', - { - defaultMessage: 'CMSTP (T1191)', - } + { defaultMessage: 'CMSTP (T1191)' } ), id: 'T1191', name: 'CMSTP', @@ -2367,9 +2347,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription', - { - defaultMessage: 'DCShadow (T1207)', - } + { defaultMessage: 'DCShadow (T1207)' } ), id: 'T1207', name: 'DCShadow', @@ -2688,9 +2666,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription', - { - defaultMessage: 'Emond (T1519)', - } + { defaultMessage: 'Emond (T1519)' } ), id: 'T1519', name: 'Emond', @@ -3053,9 +3029,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription', - { - defaultMessage: 'Hooking (T1179)', - } + { defaultMessage: 'Hooking (T1179)' } ), id: 'T1179', name: 'Hooking', @@ -3231,9 +3205,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription', - { - defaultMessage: 'Keychain (T1142)', - } + { defaultMessage: 'Keychain (T1142)' } ), id: 'T1142', name: 'Keychain', @@ -3310,9 +3282,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription', - { - defaultMessage: 'Launchctl (T1152)', - } + { defaultMessage: 'Launchctl (T1152)' } ), id: 'T1152', name: 'Launchctl', @@ -3334,9 +3304,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription', - { - defaultMessage: 'Login Item (T1162)', - } + { defaultMessage: 'Login Item (T1162)' } ), id: 'T1162', name: 'Login Item', @@ -3402,9 +3370,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription', - { - defaultMessage: 'Mshta (T1170)', - } + { defaultMessage: 'Mshta (T1170)' } ), id: 'T1170', name: 'Mshta', @@ -3778,9 +3744,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription', - { - defaultMessage: 'Rc.common (T1163)', - } + { defaultMessage: 'Rc.common (T1163)' } ), id: 'T1163', name: 'Rc.common', @@ -3835,9 +3799,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description', - { - defaultMessage: 'Regsvr32 (T1117)', - } + { defaultMessage: 'Regsvr32 (T1117)' } ), id: 'T1117', name: 'Regsvr32', @@ -3936,9 +3898,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rootkitDescription', - { - defaultMessage: 'Rootkit (T1014)', - } + { defaultMessage: 'Rootkit (T1014)' } ), id: 'T1014', name: 'Rootkit', @@ -3949,9 +3909,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description', - { - defaultMessage: 'Rundll32 (T1085)', - } + { defaultMessage: 'Rundll32 (T1085)' } ), id: 'T1085', name: 'Rundll32', @@ -4050,9 +4008,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription', - { - defaultMessage: 'Scripting (T1064)', - } + { defaultMessage: 'Scripting (T1064)' } ), id: 'T1064', name: 'Scripting', @@ -4217,9 +4173,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription', - { - defaultMessage: 'Source (T1153)', - } + { defaultMessage: 'Source (T1153)' } ), id: 'T1153', name: 'Source', @@ -4351,9 +4305,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription', - { - defaultMessage: 'Sudo (T1169)', - } + { defaultMessage: 'Sudo (T1169)' } ), id: 'T1169', name: 'Sudo', @@ -4529,9 +4481,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription', - { - defaultMessage: 'Timestomp (T1099)', - } + { defaultMessage: 'Timestomp (T1099)' } ), id: 'T1099', name: 'Timestomp', @@ -4564,9 +4514,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription', - { - defaultMessage: 'Trap (T1154)', - } + { defaultMessage: 'Trap (T1154)' } ), id: 'T1154', name: 'Trap', @@ -4698,9 +4646,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription', - { - defaultMessage: 'Web Shell (T1100)', - } + { defaultMessage: 'Web Shell (T1100)' } ), id: 'T1100', name: 'Web Shell', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 0cd75506fa9f5..a327f8498f7c0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -300,28 +300,40 @@ describe('rule helpers', () => { }); describe('getHumanizedDuration', () => { - test('returns from as seconds if from duration is less than a minute', () => { + test('returns from as seconds if from duration is specified in seconds', () => { const result = getHumanizedDuration('now-62s', '1m'); expect(result).toEqual('2s'); }); - test('returns from as minutes if from duration is less than an hour', () => { + test('returns from as seconds if from duration is specified in seconds greater than 60', () => { + const result = getHumanizedDuration('now-122s', '1m'); + + expect(result).toEqual('62s'); + }); + + test('returns from as minutes if from duration is specified in minutes', () => { const result = getHumanizedDuration('now-660s', '5m'); expect(result).toEqual('6m'); }); - test('returns from as hours if from duration is more than 60 minutes', () => { - const result = getHumanizedDuration('now-7400s', '5m'); + test('returns from as minutes if from duration is specified in minutes greater than 60', () => { + const result = getHumanizedDuration('now-6600s', '5m'); + + expect(result).toEqual('105m'); + }); + + test('returns from as hours if from duration is specified in hours', () => { + const result = getHumanizedDuration('now-7500s', '5m'); - expect(result).toEqual('1h'); + expect(result).toEqual('2h'); }); test('returns from as if from is not parsable as dateMath', () => { const result = getHumanizedDuration('randomstring', '5m'); - expect(result).toEqual('NaNh'); + expect(result).toEqual('NaNs'); }); test('returns from as 5m if interval is not parsable as dateMath', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 4dbcffbc807ec..ffcf25d253798 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -118,15 +118,21 @@ export const getHumanizedDuration = (from: string, interval: string): string => const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); const fromDuration = moment.duration(intervalValue.diff(fromValue)); - const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; - if (fromDuration.asSeconds() < 60) { - return `${Math.floor(fromDuration.asSeconds())}s`; - } else if (fromDuration.asMinutes() < 60) { - return `${Math.floor(fromDuration.asMinutes())}m`; + // Basing calculations off floored seconds count as moment durations weren't precise + const intervalDuration = Math.floor(fromDuration.asSeconds()); + // For consistency of display value + if (intervalDuration === 0) { + return `0s`; } - return fromHumanize; + if (intervalDuration % 3600 === 0) { + return `${intervalDuration / 3600}h`; + } else if (intervalDuration % 60 === 0) { + return `${intervalDuration / 60}m`; + } else { + return `${intervalDuration}s`; + } }; export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 822c57b92b4e5..2d0b9f759f158 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -45,9 +45,7 @@ export const TrustedAppsPage = memo(() => { return ; } return null; - // FIXME: Route state is being deleted by some parent component - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [routeState]); const addButton = ( ( - + ( - + {type !== DataProviderType.template && @@ -245,7 +245,7 @@ export const StatefulEditDataProvider = React.memo( ) : null} - + @@ -265,7 +265,7 @@ export const StatefulEditDataProvider = React.memo( }) || isValueFieldInvalid } onClick={handleSave} - size="s" + size="m" > {i18n.SAVE} diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index 5c31b3fad685a..aa4112d8a6f97 100644 --- a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js @@ -13,9 +13,10 @@ const fetch = require('node-fetch'); const { camelCase } = require('lodash'); const { resolve } = require('path'); -const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); -const MITRE_ENTREPRISE_ATTACK_URL = - 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; +const OUTPUT_DIRECTORY = resolve('public', 'detections', 'mitre'); +// Revert to https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json once we support sub-techniques +const MITRE_ENTERPRISE_ATTACK_URL = + 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v6.3/enterprise-attack/enterprise-attack.json'; const getTacticsOptions = (tactics) => tactics.map((t) => @@ -63,7 +64,7 @@ const getIdReference = (references) => ); async function main() { - fetch(MITRE_ENTREPRISE_ATTACK_URL) + fetch(MITRE_ENTERPRISE_ATTACK_URL) .then((res) => res.json()) .then((json) => { const mitreData = json.objects; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 3fc41550a1fc4..2cc8245e521bf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -9,11 +9,13 @@ import { SavedObjectsServiceStart, SavedObjectsClientContract, } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { AgentService, IngestManagerStartContract, PackageService, } from '../../../ingest_manager/server'; +import { PluginStartContract as AlertsPluginStartContract } from '../../../alerts/server'; import { getPackagePolicyCreateCallback } from './ingest_integration'; import { ManifestManager } from './services/artifacts'; import { MetadataQueryStrategy } from './types'; @@ -24,6 +26,8 @@ import { } from './routes/metadata/support/query_strategies'; import { ElasticsearchAssetType } from '../../../ingest_manager/common/types/models'; import { metadataTransformPrefix } from '../../common/endpoint/constants'; +import { AppClientFactory } from '../client'; +import { ConfigType } from '../config'; export interface MetadataService { queryStrategy( @@ -70,6 +74,10 @@ export type EndpointAppContextServiceStartContract = Partial< > & { logger: Logger; manifestManager?: ManifestManager; + appClientFactory: AppClientFactory; + security: SecurityPluginSetup; + alerts: AlertsPluginStartContract; + config: ConfigType; registerIngestCallback?: IngestManagerStartContract['registerExternalCallback']; savedObjectsStart: SavedObjectsServiceStart; }; @@ -93,7 +101,14 @@ export class EndpointAppContextService { if (this.manifestManager && dependencies.registerIngestCallback) { dependencies.registerIngestCallback( 'packagePolicyCreate', - getPackagePolicyCreateCallback(dependencies.logger, this.manifestManager) + getPackagePolicyCreateCallback( + dependencies.logger, + this.manifestManager, + dependencies.appClientFactory, + dependencies.config.maxTimelineImportExportSize, + dependencies.security, + dependencies.alerts + ) ); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index c28ffcf5b7a3f..1db3e9984284d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggingSystemMock } from 'src/core/server/mocks'; +import { httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { createNewPackagePolicyMock } from '../../../ingest_manager/common/mocks'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { @@ -12,8 +12,23 @@ import { ManifestManagerMockType, } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackagePolicyCreateCallback } from './ingest_integration'; +import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; +import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; +import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services'; +import { createMockEndpointAppContextServiceStartContract } from './mocks'; describe('ingest_integration tests ', () => { + let endpointAppContextMock: EndpointAppContextServiceStartContract; + let req: KibanaRequest; + let ctx: RequestHandlerContext; + const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize; + + beforeEach(() => { + endpointAppContextMock = createMockEndpointAppContextServiceStartContract(); + ctx = requestContextMock.createTools().context; + req = httpServerMock.createKibanaRequest(); + }); + describe('ingest_integration sanity checks', () => { test('policy is updated with initial manifest', async () => { const logger = loggingSystemMock.create().get('ingest_integration.test'); @@ -21,9 +36,16 @@ describe('ingest_integration tests ', () => { mockType: ManifestManagerMockType.InitialSystemState, }); - const callback = getPackagePolicyCreateCallback(logger, manifestManager); + const callback = getPackagePolicyCreateCallback( + logger, + manifestManager, + endpointAppContextMock.appClientFactory, + maxTimelineImportExportSize, + endpointAppContextMock.security, + endpointAppContextMock.alerts + ); const policyConfig = createNewPackagePolicyMock(); // policy config without manifest - const newPolicyConfig = await callback(policyConfig); // policy config WITH manifest + const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); @@ -91,9 +113,16 @@ describe('ingest_integration tests ', () => { manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error('error updating')]); const lastComputed = await manifestManager.getLastComputedManifest(); - const callback = getPackagePolicyCreateCallback(logger, manifestManager); + const callback = getPackagePolicyCreateCallback( + logger, + manifestManager, + endpointAppContextMock.appClientFactory, + maxTimelineImportExportSize, + endpointAppContextMock.security, + endpointAppContextMock.alerts + ); const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig); + const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); @@ -111,9 +140,16 @@ describe('ingest_integration tests ', () => { expect(lastComputed).toEqual(null); manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error('abcd')); - const callback = getPackagePolicyCreateCallback(logger, manifestManager); + const callback = getPackagePolicyCreateCallback( + logger, + manifestManager, + endpointAppContextMock.appClientFactory, + maxTimelineImportExportSize, + endpointAppContextMock.security, + endpointAppContextMock.alerts + ); const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig); + const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); @@ -125,9 +161,16 @@ describe('ingest_integration tests ', () => { const lastComputed = await manifestManager.getLastComputedManifest(); manifestManager.buildNewManifest = jest.fn().mockResolvedValue(lastComputed); // no diffs - const callback = getPackagePolicyCreateCallback(logger, manifestManager); + const callback = getPackagePolicyCreateCallback( + logger, + manifestManager, + endpointAppContextMock.appClientFactory, + maxTimelineImportExportSize, + endpointAppContextMock.security, + endpointAppContextMock.alerts + ); const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig); + const newPolicyConfig = await callback(policyConfig, ctx, req); expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory()); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 489b146daeb43..279603cd621c8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../src/core/server'; +import { PluginStartContract as AlertsStartContract } from '../../../alerts/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { ExternalCallback } from '../../../ingest_manager/server'; +import { KibanaRequest, Logger, RequestHandlerContext } from '../../../../../src/core/server'; import { NewPackagePolicy } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; @@ -13,6 +16,10 @@ import { Manifest } from './lib/artifacts'; import { reportErrors } from './lib/artifacts/common'; import { InternalArtifactCompleteSchema } from './schemas/artifacts'; import { manifestDispatchSchema } from '../../common/endpoint/schema/manifest'; +import { AppClientFactory } from '../client'; +import { createDetectionIndex } from '../lib/detection_engine/routes/index/create_index_route'; +import { createPrepackagedRules } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { buildFrameworkRequest } from '../lib/timeline/routes/utils/common'; const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => { let manifest: Manifest | null = null; @@ -71,19 +78,52 @@ const getManifest = async (logger: Logger, manifestManager: ManifestManager): Pr */ export const getPackagePolicyCreateCallback = ( logger: Logger, - manifestManager: ManifestManager -): ((newPackagePolicy: NewPackagePolicy) => Promise) => { + manifestManager: ManifestManager, + appClientFactory: AppClientFactory, + maxTimelineImportExportSize: number, + securitySetup: SecurityPluginSetup, + alerts: AlertsStartContract +): ExternalCallback[1] => { const handlePackagePolicyCreate = async ( - newPackagePolicy: NewPackagePolicy + newPackagePolicy: NewPackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest ): Promise => { // We only care about Endpoint package policies if (newPackagePolicy.package?.name !== 'endpoint') { return newPackagePolicy; } - // We cast the type here so that any changes to the Endpoint specific data - // follow the types/schema expected - let updatedPackagePolicy = newPackagePolicy as NewPolicyData; + // prep for detection rules creation + const appClient = appClientFactory.create(request); + const frameworkRequest = await buildFrameworkRequest(context, securitySetup, request); + + // Create detection index & rules (if necessary). move past any failure, this is just a convenience + try { + await createDetectionIndex(context, appClient); + } catch (err) { + if (err.statusCode !== 409) { + // 409 -> detection index already exists, which is fine + logger.warn( + `Possible problem creating detection signals index (${err.statusCode}): ${err.message}` + ); + } + } + try { + // this checks to make sure index exists first, safe to try in case of failure above + // may be able to recover from minor errors + await createPrepackagedRules( + context, + appClient, + alerts.getAlertsClientWithRequest(request), + frameworkRequest, + maxTimelineImportExportSize + ); + } catch (err) { + logger.error( + `Unable to create detection rules automatically (${err.statusCode}): ${err.message}` + ); + } // Get most recent manifest const manifest = await getManifest(logger, manifestManager); @@ -94,6 +134,10 @@ export const getPackagePolicyCreateCallback = ( logger.error('Invalid manifest'); } + // We cast the type here so that any changes to the Endpoint specific data + // follow the types/schema expected + let updatedPackagePolicy = newPackagePolicy as NewPolicyData; + // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. updatedPackagePolicy = { diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 9fd1fb26b1c58..98b971a00710d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,6 +6,8 @@ import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { alertsMock } from '../../../alerts/server/mocks'; import { xpackMocks } from '../../../../mocks'; import { AgentService, @@ -14,6 +16,7 @@ import { PackageService, } from '../../../ingest_manager/server'; import { createPackagePolicyServiceMock } from '../../../ingest_manager/server/mocks'; +import { AppClientFactory } from '../client'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, @@ -57,12 +60,19 @@ export const createMockEndpointAppContextService = ( export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< EndpointAppContextServiceStartContract > => { + const factory = new AppClientFactory(); + const config = createMockConfig(); + factory.setup({ getSpaceId: () => 'mockSpace', config }); return { agentService: createMockAgentService(), packageService: createMockPackageService(), logger: loggingSystemMock.create().get('mock_endpoint_app_context'), savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), + appClientFactory: factory, + security: securityMock.createSetup(), + alerts: alertsMock.createStart(), + config, registerIngestCallback: jest.fn< ReturnType, Parameters diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts index 473a2dad37f19..e7618f155967b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts @@ -6,20 +6,19 @@ import { get } from 'lodash'; import { LegacyAPICaller } from '../../../../../../../../src/core/server'; -import { getSignalsTemplate } from './get_signals_template'; import { getTemplateExists } from '../../index/get_template_exists'; +import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; export const templateNeedsUpdate = async (callCluster: LegacyAPICaller, index: string) => { const templateExists = await getTemplateExists(callCluster, index); - let existingTemplateVersion: number | undefined; - if (templateExists) { - const existingTemplate: unknown = await callCluster('indices.getTemplate', { - name: index, - }); - existingTemplateVersion = get(existingTemplate, [index, 'version']); + if (!templateExists) { + return true; } - const newTemplate = getSignalsTemplate(index); - if (existingTemplateVersion === undefined || existingTemplateVersion < newTemplate.version) { + const existingTemplate: unknown = await callCluster('indices.getTemplate', { + name: index, + }); + const existingTemplateVersion: number | undefined = get(existingTemplate, [index, 'version']); + if (existingTemplateVersion === undefined || existingTemplateVersion < SIGNALS_TEMPLATE_VERSION) { return true; } return false; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index a801bc18db439..287459cf5ec9a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../src/core/server'; +import { AppClient } from '../../../../types'; +import { IRouter, RequestHandlerContext } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { setPolicy } from '../../index/set_policy'; import { setTemplate } from '../../index/set_template'; -import { getSignalsTemplate } from './get_signals_template'; +import { getSignalsTemplate, SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; import { createBootstrapIndex } from '../../index/create_bootstrap_index'; import signalsPolicy from './signals_policy.json'; import { templateNeedsUpdate } from './check_template_version'; +import { getIndexVersion } from './get_index_version'; export const createIndexRoute = (router: IRouter) => { router.post( @@ -29,29 +31,11 @@ export const createIndexRoute = (router: IRouter) => { const siemResponse = buildSiemResponse(response); try { - const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution?.getAppClient(); - const callCluster = clusterClient.callAsCurrentUser; - if (!siemClient) { return siemResponse.error({ statusCode: 404 }); } - - const index = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(callCluster, index); - if (await templateNeedsUpdate(callCluster, index)) { - const policyExists = await getPolicyExists(callCluster, index); - if (!policyExists) { - await setPolicy(callCluster, index, signalsPolicy); - } - await setTemplate(callCluster, index, getSignalsTemplate(index)); - if (indexExists) { - await callCluster('indices.rollover', { alias: index }); - } - } - if (!indexExists) { - await createBootstrapIndex(callCluster, index); - } + await createDetectionIndex(context, siemClient!); return response.ok({ body: { acknowledged: true } }); } catch (err) { const error = transformError(err); @@ -63,3 +47,41 @@ export const createIndexRoute = (router: IRouter) => { } ); }; + +class CreateIndexError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +export const createDetectionIndex = async ( + context: RequestHandlerContext, + siemClient: AppClient +): Promise => { + const clusterClient = context.core.elasticsearch.legacy.client; + const callCluster = clusterClient.callAsCurrentUser; + + if (!siemClient) { + throw new CreateIndexError('', 404); + } + + const index = siemClient.getSignalsIndex(); + const policyExists = await getPolicyExists(callCluster, index); + if (!policyExists) { + await setPolicy(callCluster, index, signalsPolicy); + } + if (await templateNeedsUpdate(callCluster, index)) { + await setTemplate(callCluster, index, getSignalsTemplate(index)); + } + const indexExists = await getIndexExists(callCluster, index); + if (indexExists) { + const indexVersion = await getIndexVersion(callCluster, index); + if (indexVersion !== SIGNALS_TEMPLATE_VERSION) { + await callCluster('indices.rollover', { alias: index }); + } + } else { + await createBootstrapIndex(callCluster, index); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts new file mode 100644 index 0000000000000..062cffd393555 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { LegacyAPICaller } from '../../../../../../../../src/core/server'; +import { readIndex } from '../../index/read_index'; + +interface IndicesAliasResponse { + [index: string]: IndexAliasResponse; +} + +interface IndexAliasResponse { + aliases: { + [aliasName: string]: Record; + }; +} + +export const getIndexVersion = async ( + callCluster: LegacyAPICaller, + index: string +): Promise => { + const indexAlias: IndicesAliasResponse = await callCluster('indices.getAlias', { + index, + }); + const writeIndex = Object.keys(indexAlias).find( + (key) => indexAlias[key].aliases[index].is_write_index + ); + if (writeIndex === undefined) { + return undefined; + } + const writeIndexMapping = await readIndex(callCluster, writeIndex); + return get(writeIndexMapping, [writeIndex, 'mappings', '_meta', 'version']); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index b676ab5705bfc..d1a9b701b2c9d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -7,8 +7,10 @@ import signalsMapping from './signals_mapping.json'; import ecsMapping from './ecs_mapping.json'; +export const SIGNALS_TEMPLATE_VERSION = 2; +export const MIN_EQL_RULE_INDEX_VERSION = 2; + export const getSignalsTemplate = (index: string) => { - const version = 2; const template = { settings: { index: { @@ -31,10 +33,10 @@ export const getSignalsTemplate = (index: string) => { signal: signalsMapping.mappings.properties.signal, }, _meta: { - version, + version: SIGNALS_TEMPLATE_VERSION, }, }, - version, + version: SIGNALS_TEMPLATE_VERSION, }; return template; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index b9ae8b546b8bd..d1b1a2b4dd0eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -8,7 +8,8 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { templateNeedsUpdate } from './check_template_version'; +import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; +import { getIndexVersion } from './get_index_version'; export const readIndexRoute = (router: IRouter) => { router.get( @@ -32,10 +33,24 @@ export const readIndexRoute = (router: IRouter) => { const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); - const templateOutdated = await templateNeedsUpdate(clusterClient.callAsCurrentUser, index); if (indexExists) { - return response.ok({ body: { name: index, template_outdated: templateOutdated } }); + let mappingOutdated: boolean | null = null; + try { + const indexVersion = await getIndexVersion(clusterClient.callAsCurrentUser, index); + mappingOutdated = indexVersion !== SIGNALS_TEMPLATE_VERSION; + } catch (err) { + const error = transformError(err); + // Some users may not have the view_index_metadata permission necessary to check the index mapping version + // so just continue and return null for index_mapping_outdated if the error is a 403 + if (error.statusCode !== 403) { + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + return response.ok({ body: { name: index, index_mapping_outdated: mappingOutdated } }); } else { return siemResponse.error({ statusCode: 404, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index b1f6f73b09627..f885445c29b04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from '../../../../../../../../src/core/server'; +import { AppClient } from '../../../../types'; +import { IRouter, RequestHandlerContext } from '../../../../../../../../src/core/server'; import { validate } from '../../../../../common/validate'; import { @@ -28,6 +29,8 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; import { transformError, buildSiemResponse } from '../utils'; +import { AlertsClient } from '../../../../../../alerts/server'; +import { FrameworkRequest } from '../../../framework'; export const addPrepackedRulesRoute = ( router: IRouter, @@ -48,62 +51,20 @@ export const addPrepackedRulesRoute = ( try { const alertsClient = context.alerting?.getAlertsClient(); - const clusterClient = context.core.elasticsearch.legacy.client; - const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - // This will create the endpoint list if it does not exist yet - await context.lists?.getExceptionListClient().createEndpointList(); - - const rulesFromFileSystem = getPrepackagedRules(); - const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); - const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); - const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const signalsIndex = siemClient.getSignalsIndex(); - if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { - const signalsIndexExists = await getIndexExists( - clusterClient.callAsCurrentUser, - signalsIndex - ); - if (!signalsIndexExists) { - return siemResponse.error({ - statusCode: 400, - body: `Pre-packaged rules cannot be installed until the signals index is created: ${signalsIndex}`, - }); - } - } - const result = await Promise.all([ - installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex), - installPrepackagedTimelines(config.maxTimelineImportExportSize, frameworkRequest, true), - ]); - const [prepackagedTimelinesResult, timelinesErrors] = validate( - result[1], - importTimelineResultSchema - ); - await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex); - - const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { - rules_installed: rulesToInstall.length, - rules_updated: rulesToUpdate.length, - timelines_installed: prepackagedTimelinesResult?.timelines_installed ?? 0, - timelines_updated: prepackagedTimelinesResult?.timelines_updated ?? 0, - }; - const [validated, genericErrors] = validate( - prepackagedRulesOutput, - prePackagedRulesAndTimelinesSchema + const validated = await createPrepackagedRules( + context, + siemClient, + alertsClient, + frameworkRequest, + config.maxTimelineImportExportSize ); - if (genericErrors != null && timelinesErrors != null) { - return siemResponse.error({ - statusCode: 500, - body: [genericErrors, timelinesErrors].filter((msg) => msg != null).join(', '), - }); - } else { - return response.ok({ body: validated ?? {} }); - } + return response.ok({ body: validated ?? {} }); } catch (err) { const error = transformError(err); return siemResponse.error({ @@ -114,3 +75,71 @@ export const addPrepackedRulesRoute = ( } ); }; + +class PrepackagedRulesError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +export const createPrepackagedRules = async ( + context: RequestHandlerContext, + siemClient: AppClient, + alertsClient: AlertsClient, + frameworkRequest: FrameworkRequest, + maxTimelineImportExportSize: number +): Promise => { + const clusterClient = context.core.elasticsearch.legacy.client; + const savedObjectsClient = context.core.savedObjects.client; + + if (!siemClient || !alertsClient) { + throw new PrepackagedRulesError('', 404); + } + + // This will create the endpoint list if it does not exist yet + await context.lists?.getExceptionListClient().createEndpointList(); + + const rulesFromFileSystem = getPrepackagedRules(); + const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); + const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); + const signalsIndex = siemClient.getSignalsIndex(); + if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { + const signalsIndexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex); + if (!signalsIndexExists) { + throw new PrepackagedRulesError( + `Pre-packaged rules cannot be installed until the signals index is created: ${signalsIndex}`, + 400 + ); + } + } + const result = await Promise.all([ + installPrepackagedRules(alertsClient, rulesToInstall, signalsIndex), + installPrepackagedTimelines(maxTimelineImportExportSize, frameworkRequest, true), + ]); + const [prepackagedTimelinesResult, timelinesErrors] = validate( + result[1], + importTimelineResultSchema + ); + await updatePrepackagedRules(alertsClient, savedObjectsClient, rulesToUpdate, signalsIndex); + + const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { + rules_installed: rulesToInstall.length, + rules_updated: rulesToUpdate.length, + timelines_installed: prepackagedTimelinesResult?.timelines_installed ?? 0, + timelines_updated: prepackagedTimelinesResult?.timelines_updated ?? 0, + }; + const [validated, genericErrors] = validate( + prepackagedRulesOutput, + prePackagedRulesAndTimelinesSchema + ); + if (genericErrors != null && timelinesErrors != null) { + throw new PrepackagedRulesError( + [genericErrors, timelinesErrors].filter((msg) => msg != null).join(', '), + 500 + ); + } + return validated; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 6768e9534a87e..977dad680f8a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -7,7 +7,9 @@ import Boom from 'boom'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; + +import { alertsClientMock } from '../../../../../alerts/server/mocks'; +import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types'; import { BadRequestError } from '../errors/bad_request_error'; import { transformError, @@ -19,8 +21,14 @@ import { transformImportError, convertToSnakeCase, SiemResponseFactory, + mergeStatuses, + getFailingRules, } from './utils'; import { responseMock } from './__mocks__'; +import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results'; +import { getResult } from './__mocks__/request_responses'; + +let alertsClient: ReturnType; describe('utils', () => { describe('transformError', () => { @@ -319,7 +327,7 @@ describe('utils', () => { saved_objects: [], }; expect( - convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not + convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not ).toEqual(null); }); }); @@ -350,4 +358,133 @@ describe('utils', () => { ); }); }); + + describe('mergeStatuses', () => { + it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { + const statusOne = exampleRuleStatus(); + statusOne.attributes.status = 'failed'; + const statusTwo = exampleRuleStatus(); + statusTwo.attributes.status = 'failed'; + const currentStatus = exampleRuleStatus(); + const foundRules = exampleFindRuleStatusResponse([currentStatus, statusOne, statusTwo]); + const res = mergeStatuses(currentStatus.attributes.alertId, foundRules.saved_objects, { + 'myfakealertid-8cfac': { + current_status: { + alert_id: 'myfakealertid-8cfac', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [], + }, + }); + expect(res).toEqual({ + 'myfakealertid-8cfac': { + current_status: { + alert_id: 'myfakealertid-8cfac', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [], + }, + 'f4b8e31d-cf93-4bde-a265-298bde885cd7': { + current_status: { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [ + { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'failed', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'failed', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + ], + }, + }); + }); + }); + + describe('getFailingRules', () => { + beforeEach(() => { + alertsClient = alertsClientMock.create(); + }); + it('getFailingRules finds no failing rules', async () => { + alertsClient.get.mockResolvedValue(getResult()); + const res = await getFailingRules(['my-fake-id'], alertsClient); + expect(res).toEqual({}); + }); + it('getFailingRules finds a failing rule', async () => { + const foundRule = getResult(); + foundRule.executionStatus = { + status: 'error', + lastExecutionDate: foundRule.executionStatus.lastExecutionDate, + error: { + reason: 'read', + message: 'oops', + }, + }; + alertsClient.get.mockResolvedValue(foundRule); + const res = await getFailingRules([foundRule.id], alertsClient); + expect(res).toEqual({ [foundRule.id]: foundRule }); + }); + it('getFailingRules throws an error', async () => { + alertsClient.get.mockImplementation(() => { + throw new Error('my test error'); + }); + let error; + try { + await getFailingRules(['my-fake-id'], alertsClient); + } catch (exc) { + error = exc; + } + expect(error.message).toEqual( + 'Failed to get executionStatus with AlertsClient: my test error' + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index 96f96d7ebcc9e..72be7a3c0fa08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -17,7 +17,7 @@ import { } from '../../../../../../../src/core/server'; import { AlertsClient } from '../../../../../alerts/server'; import { BadRequestError } from '../errors/bad_request_error'; -import { RuleStatusResponse, IRuleStatusAttributes } from '../rules/types'; +import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; export interface OutputError { message: string; @@ -294,39 +294,53 @@ export const convertToSnakeCase = >( }, {}); }; +/** + * + * @param id rule id + * @param currentStatusAndFailures array of rule statuses where the 0th status is the current status and 1-5 positions are the historical failures + * @param acc accumulated rule id : statuses + */ export const mergeStatuses = ( id: string, - failures: Array>, + currentStatusAndFailures: Array>, acc: RuleStatusResponse -) => { - if (failures.length === 0) { +): RuleStatusResponse => { + if (currentStatusAndFailures.length === 0) { return { ...acc, }; } - const convertedCurrentStatus = convertToSnakeCase(failures[0].attributes); + const convertedCurrentStatus = convertToSnakeCase( + currentStatusAndFailures[0].attributes + ); return { ...acc, [id]: { current_status: convertedCurrentStatus, - failures: failures.map((errorItem) => - convertToSnakeCase(errorItem.attributes) - ), + failures: currentStatusAndFailures + .slice(1) + .map((errorItem) => convertToSnakeCase(errorItem.attributes)), }, } as RuleStatusResponse; }; -export const getFailingRules = (ids: string[], alertsClient: AlertsClient) => - Promise.all( - ids.map(async (id) => - alertsClient.get({ - id, - }) - ) - ) - .then((rules) => rules.filter((rule) => rule.executionStatus.status === 'error')) - .then((rules) => - rules.reduce((acc, failingRule) => { +export type GetFailingRulesResult = Record; + +export const getFailingRules = async ( + ids: string[], + alertsClient: AlertsClient +): Promise => { + try { + const errorRules = await Promise.all( + ids.map(async (id) => + alertsClient.get({ + id, + }) + ) + ); + return errorRules + .filter((rule) => rule.executionStatus.status === 'error') + .reduce((acc, failingRule) => { const accum = acc; const theRule = failingRule; return { @@ -335,5 +349,8 @@ export const getFailingRules = (ids: string[], alertsClient: AlertsClient) => }, ...accum, }; - }, {} as Record) - ); + }, {}); + } catch (exc) { + throw new Error(`Failed to get executionStatus with AlertsClient: ${exc.message}`); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 8af622e6a128b..fb4763a982f43 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -105,7 +105,7 @@ export interface RuleAlertType extends Alert { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface IRuleStatusAttributes extends Record { +export interface IRuleStatusSOAttributes extends Record { alertId: string; // created alert id. statusDate: StatusDate; lastFailureAt: LastFailureAt | null | undefined; @@ -119,21 +119,35 @@ export interface IRuleStatusAttributes extends Record { searchAfterTimeDurations: string[] | null | undefined; } +export interface IRuleStatusResponseAttributes { + alert_id: string; // created alert id. + status_date: StatusDate; + last_failure_at: LastFailureAt | null | undefined; + last_failure_message: LastFailureMessage | null | undefined; + last_success_at: LastSuccessAt | null | undefined; + last_success_message: LastSuccessMessage | null | undefined; + status: JobStatus | null | undefined; + last_look_back_date: string | null | undefined; + gap: string | null | undefined; + bulk_create_time_durations: string[] | null | undefined; + search_after_time_durations: string[] | null | undefined; +} + export interface RuleStatusResponse { [key: string]: { - current_status: IRuleStatusAttributes | null | undefined; - failures: IRuleStatusAttributes[] | null | undefined; + current_status: IRuleStatusResponseAttributes | null | undefined; + failures: IRuleStatusResponseAttributes[] | null | undefined; }; } export interface IRuleSavedAttributesSavedObjectAttributes - extends IRuleStatusAttributes, + extends IRuleStatusSOAttributes, SavedObjectAttributes {} export interface IRuleStatusSavedObject { type: string; id: string; - attributes: Array>; + attributes: Array>; references: unknown[]; updated_at: string; version: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index cbf70f3119b31..4559a658c9583 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -19,7 +19,7 @@ import { } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams } from '../../types'; -import { IRuleStatusAttributes } from '../../rules/types'; +import { IRuleStatusSOAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; @@ -555,7 +555,7 @@ export const sampleDocSearchResultsWithSortId = ( export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; -export const exampleRuleStatus: () => SavedObject = () => ({ +export const exampleRuleStatus: () => SavedObject = () => ({ type: ruleStatusSavedObjectType, id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', attributes: { @@ -577,8 +577,10 @@ export const exampleRuleStatus: () => SavedObject = () => }); export const exampleFindRuleStatusResponse: ( - mockStatuses: Array> -) => SavedObjectsFindResponse = (mockStatuses = [exampleRuleStatus()]) => ({ + mockStatuses: Array> +) => SavedObjectsFindResponse = ( + mockStatuses = [exampleRuleStatus()] +) => ({ total: 1, per_page: 6, page: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts index 913efbe04aa16..1ddec9cd15148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts @@ -6,7 +6,7 @@ import { SavedObject } from 'src/core/server'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; @@ -18,7 +18,7 @@ interface RuleStatusParams { export const createNewRuleStatus = async ({ alertId, ruleStatusClient, -}: RuleStatusParams): Promise> => { +}: RuleStatusParams): Promise> => { const now = new Date().toISOString(); return ruleStatusClient.create({ alertId, @@ -38,7 +38,7 @@ export const createNewRuleStatus = async ({ export const getOrCreateRuleStatuses = async ({ alertId, ruleStatusClient, -}: RuleStatusParams): Promise>> => { +}: RuleStatusParams): Promise>> => { const ruleStatuses = await getRuleStatusSavedObjects({ alertId, ruleStatusClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts index 828b4ea41096e..72a271fb2606f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { MAX_RULE_STATUSES } from './rule_status_service'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -17,7 +17,7 @@ interface GetRuleStatusSavedObject { export const getRuleStatusSavedObjects = async ({ alertId, ruleStatusClient, -}: GetRuleStatusSavedObject): Promise> => { +}: GetRuleStatusSavedObject): Promise> => { return ruleStatusClient.find({ perPage: MAX_RULE_STATUSES, sortField: 'statusDate', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts index 4b5faeb5b9d27..f6a08852ac8d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts @@ -12,17 +12,17 @@ import { SavedObjectsFindResponse, } from '../../../../../../../src/core/server'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; export interface RuleStatusSavedObjectsClient { find: ( options?: Omit - ) => Promise>; - create: (attributes: IRuleStatusAttributes) => Promise>; + ) => Promise>; + create: (attributes: IRuleStatusSOAttributes) => Promise>; update: ( id: string, - attributes: Partial - ) => Promise>; + attributes: Partial + ) => Promise>; delete: (id: string) => Promise<{}>; } @@ -30,7 +30,10 @@ export const ruleStatusSavedObjectsClientFactory = ( savedObjectsClient: SavedObjectsClientContract ): RuleStatusSavedObjectsClient => ({ find: (options) => - savedObjectsClient.find({ ...options, type: ruleStatusSavedObjectType }), + savedObjectsClient.find({ + ...options, + type: ruleStatusSavedObjectType, + }), create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes), update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes), delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 8fdbe282eece5..433ad4e2affea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -6,7 +6,7 @@ import { assertUnreachable } from '../../../../common/utility_types'; import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -30,9 +30,9 @@ export const buildRuleStatusAttributes: ( status: JobStatus, message?: string, attributes?: Attributes -) => Partial = (status, message, attributes = {}) => { +) => Partial = (status, message, attributes = {}) => { const now = new Date().toISOString(); - const baseAttributes: Partial = { + const baseAttributes: Partial = { ...attributes, status, statusDate: now, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 838ac2558b038..bb3a0b4fa6f08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -8,7 +8,6 @@ import { Logger, KibanaRequest } from 'src/core/server'; -import { get } from 'lodash'; import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE, @@ -62,6 +61,8 @@ import { buildEqlSearchRequest } from '../../../../common/detection_engine/get_q import { bulkInsertSignals } from './single_bulk_create'; import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk_body'; import { createThreatSignals } from './threat_mapping/create_threat_signals'; +import { getIndexVersion } from '../routes/index/get_index_version'; +import { MIN_EQL_RULE_INDEX_VERSION } from '../routes/index/get_signals_template'; export const signalRulesAlertType = ({ logger, @@ -119,17 +120,6 @@ export const signalRulesAlertType = ({ type, exceptionsList, } = params; - const outputIndexTemplateMapping: unknown = await services.callCluster( - 'indices.getTemplate', - { name: outputIndex } - ); - const signalMappingVersion: number | undefined = get(outputIndexTemplateMapping, [ - outputIndex, - 'version', - ]); - if (signalMappingVersion !== undefined && typeof signalMappingVersion !== 'number') { - throw new Error('Found non-numeric value for "version" in output index template'); - } const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; @@ -457,14 +447,24 @@ export const signalRulesAlertType = ({ if (query === undefined) { throw new Error('EQL query rule must have a query defined'); } - const MIN_EQL_RULE_TEMPLATE_VERSION = 2; - if ( - signalMappingVersion === undefined || - signalMappingVersion < MIN_EQL_RULE_TEMPLATE_VERSION - ) { - throw new Error( - `EQL based rules require an update to version ${MIN_EQL_RULE_TEMPLATE_VERSION} of the detection alerts index mapping` - ); + try { + const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex); + if ( + signalIndexVersion === undefined || + signalIndexVersion < MIN_EQL_RULE_INDEX_VERSION + ) { + throw new Error( + `EQL based rules require an update to version ${MIN_EQL_RULE_INDEX_VERSION} of the detection alerts index mapping` + ); + } + } catch (err) { + if (err.statusCode === 403) { + throw new Error( + `EQL based rules require the user that created it to have the view_index_metadata, read, and write permissions for index: ${outputIndex}` + ); + } else { + throw err; + } } const inputIndex = await getInputIndex(services, version, index); const request = buildEqlSearchRequest( diff --git a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts index 6d9e9b13bc356..e36fb1144e93f 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts @@ -149,14 +149,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { } public getIndexPatternsService(request: FrameworkRequest): FrameworkIndexPatternsService { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const callCluster = async (endpoint: string, params?: Record) => - this.callWithRequest(request, endpoint, { - ...params, - allowNoIndices: true, - }); - - return new IndexPatternsFetcher(callCluster); + return new IndexPatternsFetcher(request.context.core.elasticsearch.client.asCurrentUser, true); } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f5e1c6936cbd6..43f87a0c69ab3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -23,7 +23,10 @@ import { PluginStart as DataPluginStart, } from '../../../../src/plugins/data/server'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; -import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; +import { + PluginSetupContract as AlertingSetup, + PluginStartContract as AlertPluginStartContract, +} from '../../alerts/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; @@ -88,6 +91,7 @@ export interface SetupPlugins { } export interface StartPlugins { + alerts: AlertPluginStartContract; data: DataPluginStart; ingestManager?: IngestManagerStartContract; taskManager?: TaskManagerStartContract; @@ -113,8 +117,10 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config$: Observable; + private config?: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; + private setupPlugins?: SetupPlugins; private readonly endpointAppContextService = new EndpointAppContextService(); private readonly telemetryEventsSender: TelemetryEventsSender; @@ -137,8 +143,10 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { this.logger.debug('plugin setup'); + this.setupPlugins = plugins; const config = await this.config$.pipe(first()).toPromise(); + this.config = config; const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); initSavedObjects(core.savedObjects); @@ -337,6 +345,10 @@ export class Plugin implements IPlugin(async (resolve) => { const { elasticsearch } = context.core; - const indexPatternsFetcher = new IndexPatternsFetcher( - elasticsearch.legacy.client.callAsCurrentUser - ); + const indexPatternsFetcher = new IndexPatternsFetcher(elasticsearch.client.asCurrentUser); const dedupeIndices = dedupeIndexName(request.indices); const responsesIndexFields = await Promise.all( 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 8d47d3dd30b82..a40df3b84132e 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -367,7 +367,7 @@ describe('TaskStore', () => { const { args: { - updateByQuery: { body: { query } = {} }, + updateByQuery: { body: { query, sort } = {} }, }, } = await testClaimAvailableTasks({ opts: { @@ -476,6 +476,25 @@ describe('TaskStore', () => { ], }, }); + expect(sort).toMatchObject([ + { + _script: { + type: 'number', + order: 'asc', + script: { + lang: 'painless', + source: ` +if (doc['task.retryAt'].size()!=0) { + return doc['task.retryAt'].value.toInstant().toEpochMilli(); +} +if (doc['task.runAt'].size()!=0) { + return doc['task.runAt'].value.toInstant().toEpochMilli(); +} + `, + }, + }, + }, + ]); }); test('it supports claiming specific tasks by id', async () => { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 4c41be9577ad0..63b6ab7412ec5 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -46,6 +46,7 @@ import { RangeFilter, asPinnedQuery, matchesClauses, + SortOptions, } from './queries/query_clauses'; import { @@ -272,6 +273,17 @@ export class TaskStore { ) ); + // The documents should be sorted by runAt/retryAt, unless there are pinned + // tasks being queried, in which case we want to sort by score first, and then + // the runAt/retryAt. That way we'll get the pinned tasks first. Note that + // the score seems to favor newer documents rather than older documents, so + // if there are not pinned tasks being queried, we do NOT want to sort by score + // at all, just by runAt/retryAt. + const sort: SortOptions = [SortByRunAtAndRetryAt]; + if (claimTasksById && claimTasksById.length) { + sort.unshift('_score'); + } + const apmTrans = apm.startTransaction(`taskManager markAvailableTasksAsClaimed`, 'taskManager'); const { updated } = await this.updateByQuery( asUpdateByQuery({ @@ -288,12 +300,7 @@ export class TaskStore { status: 'claiming', retryAt: claimOwnershipUntil, }), - sort: [ - // sort by score first, so the "pinned" Tasks are first - '_score', - // the nsort by other fields - SortByRunAtAndRetryAt, - ], + sort, }), { max_docs: size, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e645ae32abbd1..5cabdd62d7c87 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10996,11 +10996,9 @@ "xpack.lens.xyChart.topAxisLabel": "上の軸", "xpack.lens.xyChart.valuesLabel": "値", "xpack.lens.xyChart.xAxisGridlines.help": "x軸のグリッド線を表示するかどうかを指定します。", - "xpack.lens.xyChart.xAxisLabel": "X 軸", "xpack.lens.xyChart.xAxisTickLabels.help": "x軸の目盛ラベルを表示するかどうかを指定します。", "xpack.lens.xyChart.xAxisTitle.help": "x軸のタイトルを表示するかどうかを指定します。", "xpack.lens.xyChart.xTitle.help": "x軸のタイトル", - "xpack.lens.xyChart.yAxisLabel": "Y 軸", "xpack.lens.xyChart.yLeftAxisgridlines.help": "左y軸のグリッド線を表示するかどうかを指定します。", "xpack.lens.xyChart.yLeftAxisTickLabels.help": "左y軸の目盛ラベルを表示するかどうかを指定します。", "xpack.lens.xyChart.yLeftAxisTitle.help": "左y軸のタイトルを表示するかどうかを指定します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2f701d0cde284..229938a3c1d08 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11009,11 +11009,9 @@ "xpack.lens.xyChart.topAxisLabel": "顶轴", "xpack.lens.xyChart.valuesLabel": "值", "xpack.lens.xyChart.xAxisGridlines.help": "指定 x 轴的网格线是否可见。", - "xpack.lens.xyChart.xAxisLabel": "X 轴", "xpack.lens.xyChart.xAxisTickLabels.help": "指定 x 轴的刻度标签是否可见。", "xpack.lens.xyChart.xAxisTitle.help": "指定 x 轴的标题是否可见。", "xpack.lens.xyChart.xTitle.help": "X 轴标题", - "xpack.lens.xyChart.yAxisLabel": "Y 轴", "xpack.lens.xyChart.yLeftAxisgridlines.help": "指定左侧 y 轴的网格线是否可见。", "xpack.lens.xyChart.yLeftAxisTickLabels.help": "指定左侧 y 轴的刻度标签是否可见。", "xpack.lens.xyChart.yLeftAxisTitle.help": "指定左侧 y 轴的标题是否可见。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx index 2685e62eb9a6c..2ef887d0627cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -74,4 +74,51 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeFalsy(); expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); }); + + test('should display a message to remember username and password when creating a connector with authentication', () => { + const actionConnector = { + actionTypeId: '.email', + config: { + hasAuth: true, + }, + secrets: {}, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message when editing an authenticated email connector explaining why username and password must be re-entered', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 1e92e9fc2519c..111f6c9a47da9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -12,6 +12,7 @@ import { EuiFieldPassword, EuiSwitch, EuiFormRow, + EuiText, EuiTitle, EuiSpacer, EuiCallOut, @@ -56,7 +57,7 @@ export const EmailActionConnectorFields: React.FunctionComponent } @@ -202,17 +203,7 @@ export const EmailActionConnectorFields: React.FunctionComponent {hasAuth ? ( <> - {action.id ? ( - <> - - - - - ) : null} + {getEncryptedFieldNotifyLabel(!action.id)} + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { EmailActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 61923d8f78b51..f476522c2bf5a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -68,7 +68,7 @@ describe('jira connector validation', () => { errors: { apiUrl: ['URL is required.'], email: [], - apiToken: ['API token or Password is required'], + apiToken: ['API token or password is required'], projectKey: ['Project key is required'], }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index 2cac1819d552d..6d055f4fdb9f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -96,4 +96,60 @@ describe('JiraActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.jira', + isPreconfigured: false, + secrets: {}, + config: {}, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index 2ab9843c143b9..35c6bd7af00ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -6,12 +6,15 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldPassword, EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -133,6 +136,20 @@ const JiraConnectorFields: React.FC + + + +

{i18n.JIRA_AUTHENTICATION_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.JIRA_REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { JiraConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 019133b03d55f..f6db56e188322 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const JIRA_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', { - defaultMessage: 'Push or update data to a new issue in Jira', + defaultMessage: 'Create an incident in Jira.', } ); @@ -55,31 +55,54 @@ export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( } ); +export const JIRA_AUTHENTICATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel', + { + defaultMessage: 'Authentication', + } +); + +export const JIRA_REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const JIRA_REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel', + { + defaultMessage: + 'Authentication credentials are encrypted. Please reenter values for these fields.', + } +); + export const JIRA_EMAIL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', { - defaultMessage: 'Email or Username', + defaultMessage: 'Username or email address', } ); export const JIRA_EMAIL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', { - defaultMessage: 'Email or Username is required', + defaultMessage: 'Username or email address is required', } ); export const JIRA_API_TOKEN_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', { - defaultMessage: 'API token or Password', + defaultMessage: 'API token or password', } ); export const JIRA_API_TOKEN_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', { - defaultMessage: 'API token or Password is required', + defaultMessage: 'API token or password is required', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx index 53e68e6453690..18978be7b4680 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -49,4 +49,56 @@ describe('PagerDutyActionConnectorFields renders', () => { ); expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.pagerduty', + secrets: {}, + config: {}, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http:\\test', + }, + } as PagerDutyActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 6399e1f80984c..ad2d5b3be5268 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -53,7 +53,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent } @@ -66,26 +66,61 @@ const PagerDutyActionConnectorFields: React.FunctionComponent - 0 && routingKey !== undefined} - name="routingKey" - readOnly={readOnly} - value={routingKey || ''} - data-test-subj="pagerdutyRoutingKeyInput" - onChange={(e: React.ChangeEvent) => { - editActionSecrets('routingKey', e.target.value); - }} - onBlur={() => { - if (!routingKey) { - editActionSecrets('routingKey', ''); - } - }} - /> + + {getEncryptedFieldNotifyLabel(!action.id)} + 0 && routingKey !== undefined} + name="routingKey" + readOnly={readOnly} + value={routingKey || ''} + data-test-subj="pagerdutyRoutingKeyInput" + onChange={(e: React.ChangeEvent) => { + editActionSecrets('routingKey', e.target.value); + }} + onBlur={() => { + if (!routingKey) { + editActionSecrets('routingKey', ''); + } + }} + /> + ); }; +function getEncryptedFieldNotifyLabel(isCreate: boolean) { + if (isCreate) { + return ( + + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { PagerDutyActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index b73eb72f137c1..937fe61e887ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -68,7 +68,7 @@ describe('resilient connector validation', () => { errors: { apiUrl: ['URL is required.'], apiKeyId: [], - apiKeySecret: ['API key secret is required'], + apiKeySecret: ['Secret is required'], orgId: ['Organization ID is required'], }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index 7e242f1f501d8..6dede85736fcd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -97,4 +97,60 @@ describe('ResilientActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.resilient', + isPreconfigured: false, + config: {}, + secrets: {}, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx index 7965e216f1d6c..fe2aa341a7111 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -6,12 +6,15 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldPassword, EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -133,6 +136,20 @@ const ResilientConnectorFields: React.FC + + + +

{i18n.API_KEY_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { ResilientConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts index 71ad05abfdecf..b45f898d7d809 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', { - defaultMessage: 'Push or update data to a new incident in Resilient.', + defaultMessage: 'Create an incident in IBM Resilient.', } ); @@ -55,31 +55,53 @@ export const ORG_ID_REQUIRED = i18n.translate( } ); +export const API_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey', + { + defaultMessage: 'API key', + } +); + +export const REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel', + { + defaultMessage: 'ID and secret are encrypted. Please reenter values for these fields.', + } +); + export const API_KEY_ID_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId', { - defaultMessage: 'API key ID', + defaultMessage: 'ID', } ); export const API_KEY_ID_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField', { - defaultMessage: 'API key ID is required', + defaultMessage: 'ID is required', } ); export const API_KEY_SECRET_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret', { - defaultMessage: 'API key secret', + defaultMessage: 'Secret', } ); export const API_KEY_SECRET_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField', { - defaultMessage: 'API key secret is required', + defaultMessage: 'Secret is required', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 216e6967833b2..b666db4024b12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -83,4 +83,59 @@ describe('ServiceNowActionConnectorFields renders', () => { wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 ).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.servicenow', + isPreconfigured: false, + config: {}, + secrets: {}, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'servicenow', + config: { + apiUrl: 'https://test/', + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a8f1ed8d55447..d351b32c3bb06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -6,6 +6,7 @@ import React, { useCallback } from 'react'; import { + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -13,6 +14,8 @@ import { EuiFieldPassword, EuiSpacer, EuiLink, + EuiText, + EuiTitle, } from '@elastic/eui'; import { isEmpty } from 'lodash'; @@ -89,7 +92,7 @@ const ServiceNowConnectorFields: React.FC } @@ -113,6 +116,20 @@ const ServiceNowConnectorFields: React.FC + + + +

{i18n.AUTHENTICATION_LABEL}

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + {i18n.REMEMBER_VALUES_LABEL} + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 48544945836d9..67e94bc136cf9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -41,6 +41,28 @@ export const API_URL_INVALID = i18n.translate( } ); +export const AUTHENTICATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel', + { + defaultMessage: 'Authentication', + } +); + +export const REMEMBER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.rememberValuesLabel', + { + defaultMessage: + 'Remember these values. You must reenter them each time you edit the connector.', + } +); + +export const REENTER_VALUES_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel', + { + defaultMessage: 'Username and password are encrypted. Please reenter values for these fields.', + } +); + export const USERNAME_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx index 5bc778830b6e6..f87c2ad99fb4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -44,4 +44,54 @@ describe('SlackActionFields renders', () => { 'http:\\test' ); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.email', + config: {}, + secrets: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + webhookUrl: 'http:\\test', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: {}, + } as SlackActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index aa3a1932eacdb..3e744eff07797 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -27,7 +27,7 @@ const SlackActionFields: React.FunctionComponent } @@ -40,27 +40,62 @@ const SlackActionFields: React.FunctionComponent - 0 && webhookUrl !== undefined} - name="webhookUrl" - readOnly={readOnly} - placeholder="Example: https://hooks.slack.com/services" - value={webhookUrl || ''} - data-test-subj="slackWebhookUrlInput" - onChange={(e) => { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> + + {getEncryptedFieldNotifyLabel(!action.id)} + 0 && webhookUrl !== undefined} + name="webhookUrl" + readOnly={readOnly} + placeholder="Example: https://hooks.slack.com/services" + value={webhookUrl || ''} + data-test-subj="slackWebhookUrlInput" + onChange={(e) => { + editActionSecrets('webhookUrl', e.target.value); + }} + onBlur={() => { + if (!webhookUrl) { + editActionSecrets('webhookUrl', ''); + } + }} + /> + ); }; +function getEncryptedFieldNotifyLabel(isCreate: boolean) { + if (isCreate) { + return ( + + + + + + + + ); + } + return ( + + + + + + ); +} + // eslint-disable-next-line import/no-default-export export { SlackActionFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 7f2bed6c41f3b..45e4c566f7a27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -44,4 +44,55 @@ describe('WebhookActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + secrets: {}, + actionTypeId: '.webhook', + isPreconfigured: false, + config: {}, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 52160441adb5b..e4f5ef023a529 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiCallOut, EuiFieldPassword, EuiFieldText, EuiFormRow, @@ -18,6 +19,7 @@ import { EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, + EuiText, EuiTitle, EuiSwitch, EuiButtonEmpty, @@ -266,6 +268,26 @@ const WebhookActionConnectorFields: React.FunctionComponent + + + + +

+ +

+
+
+
+ + + + {getEncryptedFieldNotifyLabel(!action.id)} + + + + + + ); + } + return ( + + ); +} + // eslint-disable-next-line import/no-default-export export { WebhookActionConnectorFields as default }; diff --git a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx index 16853211433ca..18e058e305606 100644 --- a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx @@ -6,7 +6,16 @@ import React from 'react'; import { UptimeDatePicker } from '../uptime_date_picker'; -import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib'; +import { + renderWithRouter, + shallowWithRouter, + MountWithReduxProvider, + mountWithRouterRedux, +} from '../../../lib'; +import { UptimeStartupPluginsContextProvider } from '../../../contexts'; +import { startPlugins } from '../../../lib/__mocks__/uptime_plugin_start_mock'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { createMemoryHistory } from 'history'; describe('UptimeDatePicker component', () => { it('validates props with shallow render', () => { @@ -22,4 +31,59 @@ describe('UptimeDatePicker component', () => { ); expect(component).toMatchSnapshot(); }); + + it('uses shared date range state when there is no url date range state', () => { + const customHistory = createMemoryHistory(); + jest.spyOn(customHistory, 'push'); + + const component = mountWithRouterRedux( + )} + > + + , + { customHistory } + ); + + const startBtn = component.find('[data-test-subj="superDatePickerstartDatePopoverButton"]'); + + expect(startBtn.text()).toBe('~ 30 minutes ago'); + + const endBtn = component.find('[data-test-subj="superDatePickerendDatePopoverButton"]'); + + expect(endBtn.text()).toBe('~ 15 minutes ago'); + + expect(customHistory.push).toHaveBeenCalledWith({ + pathname: '/', + search: 'dateRangeStart=now-30m&dateRangeEnd=now-15m', + }); + }); + + it('should use url date range even if shared date range is present', () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const component = mountWithRouterRedux( + )} + > + + , + { customHistory } + ); + + const showDateBtn = component.find('[data-test-subj="superDatePickerShowDatesButton"]'); + + expect(showDateBtn.childAt(0).text()).toBe('Last 10 minutes'); + + // it should update shared state + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-10m', + to: 'now', + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx index 1d0dcad73795b..cc8d6271abd73 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx @@ -4,11 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { EuiSuperDatePicker } from '@elastic/eui'; import { useUrlParams } from '../../hooks'; import { CLIENT_DEFAULTS } from '../../../common/constants'; -import { UptimeRefreshContext, UptimeSettingsContext } from '../../contexts'; +import { + UptimeRefreshContext, + UptimeSettingsContext, + UptimeStartupPluginsContext, +} from '../../contexts'; export interface CommonlyUsedRange { from: string; @@ -16,12 +20,43 @@ export interface CommonlyUsedRange { display: string; } +const isUptimeDefaultDateRange = (dateRangeStart: string, dateRangeEnd: string) => { + const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS; + + return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END; +}; + export const UptimeDatePicker = () => { const [getUrlParams, updateUrl] = useUrlParams(); - const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams(); const { commonlyUsedRanges } = useContext(UptimeSettingsContext); const { refreshApp } = useContext(UptimeRefreshContext); + const { data } = useContext(UptimeStartupPluginsContext); + + // read time from state and update the url + const sharedTimeState = data?.query.timefilter.timefilter.getTime(); + + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart: start, + dateRangeEnd: end, + } = getUrlParams(); + + useEffect(() => { + const { from, to } = sharedTimeState ?? {}; + // if it's uptime default range, and we have shared state from kibana, let's use that + if (isUptimeDefaultDateRange(start, end) && (from !== start || to !== end)) { + updateUrl({ dateRangeStart: from, dateRangeEnd: to }); + } else if (from !== start || to !== end) { + // if it's coming url. let's update shared state + data?.query.timefilter.timefilter.setTime({ from: start, to: end }); + } + + // only need at start, rest date picker on change fucn will take care off + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const euiCommonlyUsedRanges = commonlyUsedRanges ? commonlyUsedRanges.map( ({ from, to, display }: { from: string; to: string; display: string }) => { @@ -36,13 +71,17 @@ export const UptimeDatePicker = () => { return ( { - updateUrl({ dateRangeStart: start, dateRangeEnd: end }); + onTimeChange={({ start: startN, end: endN }) => { + if (data?.query?.timefilter?.timefilter) { + data?.query.timefilter.timefilter.setTime({ from: startN, to: endN }); + } + + updateUrl({ dateRangeStart: startN, dateRangeEnd: endN }); refreshApp(); }} onRefresh={refreshApp} diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts new file mode 100644 index 0000000000000..6d2ea80a3b6f2 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface InputTimeRange { + from: string; + to: string; +} + +export const startPlugins = { + data: { + query: { + timefilter: { + timefilter: { + getTime: () => ({ to: 'now-15m', from: 'now-30m' }), + setTime: jest.fn(({ from, to }: InputTimeRange) => {}), + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx index 7da570e909425..5219fb3242539 100644 --- a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx @@ -20,22 +20,19 @@ const helperWithRouter: ( wrapReduxStore?: boolean, storeState?: AppState ) => R = (helper, component, customHistory, wrapReduxStore, storeState) => { - if (customHistory) { - customHistory.location.key = 'TestKeyForTesting'; - return helper({component}); - } - const history = createMemoryHistory(); + const history = customHistory ?? createMemoryHistory(); + history.location.key = 'TestKeyForTesting'; + const routerWrapper = {component}; + if (wrapReduxStore) { return helper( - - {component} - + {routerWrapper} ); } - return helper({component}); + return helper(routerWrapper); }; export const renderWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 3fc26811d46eb..7feb916046e3a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; +import { ElasticsearchClient } from 'kibana/server'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; @@ -81,6 +82,7 @@ export const generateFilterDSL = async ( export const formatFilterString = async ( dynamicSettings: DynamicSettings, callES: ESAPICaller, + esClient: ElasticsearchClient, filters: StatusCheckFilters, search: string, libs?: UMServerLibs @@ -88,9 +90,10 @@ export const formatFilterString = async ( await generateFilterDSL( () => libs?.requests?.getIndexPattern - ? libs?.requests?.getIndexPattern({ callES, dynamicSettings }) + ? libs?.requests?.getIndexPattern({ callES, esClient, dynamicSettings }) : getUptimeIndexPattern({ callES, + esClient, dynamicSettings, }), filters, @@ -237,6 +240,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = async executor( { params: rawParams, state, services: { alertInstanceFactory } }, callES, + esClient, dynamicSettings ) { const { @@ -252,7 +256,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = timerange: oldVersionTimeRange, } = rawParams; - const filterString = await formatFilterString(dynamicSettings, callES, filters, search, libs); + const filterString = await formatFilterString( + dynamicSettings, + callES, + esClient, + filters, + search, + libs + ); const timerange = oldVersionTimeRange || { from: isAutoGenerated diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts index b8a56405ca160..390b6d347996c 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ILegacyScopedClusterClient, ElasticsearchClient } from 'kibana/server'; import { AlertExecutorOptions, AlertType, AlertTypeState } from '../../../../alerts/server'; import { savedObjectsAdapter } from '../saved_objects'; import { DynamicSettings } from '../../../common/runtime_types'; @@ -13,6 +13,7 @@ export interface UptimeAlertType extends Omit Promise; } @@ -22,13 +23,13 @@ export const uptimeAlertWrapper = (uptimeAlert: UptimeAlertType) => ({ producer: 'uptime', executor: async (options: AlertExecutorOptions) => { const { - services: { callCluster: callES }, + services: { callCluster: callES, scopedClusterClient }, } = options; const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( options.services.savedObjectsClient ); - return uptimeAlert.executor(options, callES, dynamicSettings); + return uptimeAlert.executor(options, callES, scopedClusterClient, dynamicSettings); }, }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 1d284143a1ab0..06846a73ed3d7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, LegacyCallAPIOptions } from 'src/core/server'; +import { ElasticsearchClient } from 'kibana/server'; import { UMElasticsearchQueryFn } from '../adapters'; import { IndexPatternsFetcher, FieldDescriptor } from '../../../../../../src/plugins/data/server'; @@ -14,15 +14,10 @@ export interface IndexPatternTitleAndFields { } export const getUptimeIndexPattern: UMElasticsearchQueryFn< - {}, + { esClient: ElasticsearchClient }, IndexPatternTitleAndFields | undefined -> = async ({ callES, dynamicSettings }) => { - const callAsCurrentUser: LegacyAPICaller = async ( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) => callES(endpoint, clientParams, options); - const indexPatternsFetcher = new IndexPatternsFetcher(callAsCurrentUser); +> = async ({ esClient, dynamicSettings }) => { + const indexPatternsFetcher = new IndexPatternsFetcher(esClient); // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) // and since `getFieldsForWildcard` will throw if the specified indices don't exist, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index fbcbc37ae0cc2..ec750f92656b2 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchClient } from 'kibana/server'; import { ESAPICaller, UMElasticsearchQueryFn } from '../adapters'; import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; import { formatFilterString } from '../alerts/status_check'; @@ -13,10 +14,12 @@ export interface GetMonitorDetailsParams { dateStart: string; dateEnd: string; alertsClient: any; + esClient: ElasticsearchClient; } const getMonitorAlerts = async ( callES: ESAPICaller, + esClient: ElasticsearchClient, dynamicSettings: any, alertsClient: any, monitorId: string @@ -67,6 +70,7 @@ const getMonitorAlerts = async ( const parsedFilters = await formatFilterString( dynamicSettings, callES, + esClient, currAlert.params.filters, currAlert.params.search ); @@ -84,7 +88,7 @@ const getMonitorAlerts = async ( export const getMonitorDetails: UMElasticsearchQueryFn< GetMonitorDetailsParams, MonitorDetails -> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => { +> = async ({ callES, esClient, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => { const queryFilters: any = [ { range: { @@ -134,7 +138,13 @@ export const getMonitorDetails: UMElasticsearchQueryFn< const monitorError: MonitorError | undefined = data?.error; const errorTimestamp: string | undefined = data?.['@timestamp']; - const monAlerts = await getMonitorAlerts(callES, dynamicSettings, alertsClient, monitorId); + const monAlerts = await getMonitorAlerts( + callES, + esClient, + dynamicSettings, + alertsClient, + monitorId + ); return { monitorId, error: monitorError, diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 26715f0ff37b6..baf999158a29e 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -16,7 +16,11 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer try { return response.ok({ body: { - ...(await libs.requests.getIndexPattern({ callES, dynamicSettings })), + ...(await libs.requests.getIndexPattern({ + callES, + esClient: _context.core.elasticsearch.client.asCurrentUser, + dynamicSettings, + })), }, }); } catch (e) { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index bb54effc0d57e..8bbb4fcb5575c 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -28,6 +28,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ body: { ...(await libs.requests.getMonitorDetails({ callES, + esClient: context.core.elasticsearch.client.asCurrentUser, dynamicSettings, monitorId, dateStart, diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts index ada67bbec070b..085a81c5f1bf4 100644 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts @@ -25,7 +25,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name'; // Failing: See https://github.com/elastic/kibana/issues/81264 - describe.skip('Slow durations', () => { + describe('Slow durations', () => { const url = format({ pathname: `/api/apm/correlations/slow_durations`, query: { start, end, durationPercentile, fieldNames }, @@ -40,21 +40,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - describe('with default scoring', () => { - let response: PromiseReturnType; - before(async () => { - await esArchiver.load(archiveName); - response = await supertest.get(url); - }); - + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); + describe('making request with default args', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get(url); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); - it('returns fields in response', () => { - expectSnapshot(Object.keys(response.body.response)).toMatchInline(` + it('returns fields in response', () => { + expectSnapshot(Object.keys(response.body.response)).toMatchInline(` Array [ "service.node.name", "host.ip", @@ -64,14 +64,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { "url.domain", ] `); - }); + }); - it('returns cardinality for each field', () => { - const cardinalitys = Object.values(response.body.response).map( - (field: any) => field.cardinality - ); + it('returns cardinality for each field', () => { + const cardinalitys = Object.values(response.body.response).map( + (field: any) => field.cardinality + ); - expectSnapshot(cardinalitys).toMatchInline(` + expectSnapshot(cardinalitys).toMatchInline(` Array [ 5, 6, @@ -81,11 +81,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { 4, ] `); - }); + }); - it('returns buckets', () => { - const { buckets } = response.body.response['user.id'].value; - expectSnapshot(buckets[0]).toMatchInline(` + it('returns buckets', () => { + const { buckets } = response.body.response['user.id'].value; + expectSnapshot(buckets[0]).toMatchInline(` Object { "bg_count": 32, "doc_count": 6, @@ -93,46 +93,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { "score": 0.1875, } `); + }); + }); }); - }); - describe('with different scoring', () => { - before(async () => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it(`returns buckets for each score`, async () => { - const promises = ['percentage', 'jlh', 'chi_square', 'gnd'].map(async (scoring) => { - const response = await supertest.get( - format({ - pathname: `/api/apm/correlations/slow_durations`, - query: { start, end, durationPercentile, fieldNames, scoring }, - }) - ); - - return { name: scoring, value: response.body.response['user.id'].value.buckets[0].score }; + describe('making a request for each "scoring"', () => { + ['percentage', 'jlh', 'chi_square', 'gnd'].map(async (scoring) => { + it(`returns response for scoring "${scoring}"`, async () => { + const response = await supertest.get( + format({ + pathname: `/api/apm/correlations/slow_durations`, + query: { start, end, durationPercentile, fieldNames, scoring }, + }) + ); + + expect(response.status).to.be(200); + }); }); - - const res = await Promise.all(promises); - expectSnapshot(res).toMatchInline(` - Array [ - Object { - "name": "percentage", - "value": 0.1875, - }, - Object { - "name": "jlh", - "value": 3.33506905769659, - }, - Object { - "name": "chi_square", - "value": 219.192006524483, - }, - Object { - "name": "gnd", - "value": 0.671406580688819, - }, - ] - `); }); }); }); diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js index 8dbd98ed3af2f..6a2c1f8437698 100644 --- a/x-pack/test/functional/apps/maps/discover.js +++ b/x-pack/test/functional/apps/maps/discover.js @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }) { expect(doesLayerExist).to.equal(true); const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('4'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); }); it('should link geo_point fields to Maps application with time and query context', async () => { @@ -55,6 +56,7 @@ export default function ({ getService, getPageObjects }) { expect(doesLayerExist).to.equal(true); const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('7'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); }); }); } diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index bd5ecfe2a2504..0e2850dafbccc 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -38,6 +38,7 @@ export default function ({ getPageObjects, getService }) { after(async () => { await inspector.close(); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js index dd9b93c995695..75a0e7da0f256 100644 --- a/x-pack/test/functional/apps/maps/layer_visibility.js +++ b/x-pack/test/functional/apps/maps/layer_visibility.js @@ -19,6 +19,7 @@ export default function ({ getPageObjects, getService }) { afterEach(async () => { await inspector.close(); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/vector_styling.js index 1def542982dd8..e4c5eaf892c76 100644 --- a/x-pack/test/functional/apps/maps/vector_styling.js +++ b/x-pack/test/functional/apps/maps/vector_styling.js @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.maps.loadSavedMap('document example'); }); after(async () => { + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 7be0aa425509e..408a50be8882e 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -19,6 +19,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const queryBar = getService('queryBar'); const comboBox = getService('comboBox'); const renderable = getService('renderable'); + const browser = getService('browser'); function escapeLayerName(layerName: string) { return layerName.split(' ').join('_'); @@ -692,6 +693,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } await testSubjects.click('mapSettingSubmitButton'); } + + async refreshAndClearUnsavedChangesWarning() { + await browser.refresh(); + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + } } return new GisPage(); } diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 319dc54fa6421..c3ee05db599a9 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -35,6 +35,7 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte // clicking on the testSubject const input = await find.activeElement(); + // make sure that clearing the element's value works await retry.tryForTime(5000, async () => { let currentValue = await input.getAttribute('value'); if (currentValue !== '') { @@ -53,6 +54,7 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte } }); + // make sure that typing a character really adds that character to the input value for (const chr of text) { await retry.tryForTime(5000, async () => { const oldValue = await input.getAttribute('value'); @@ -70,6 +72,16 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte }); }); } + + // make sure that finally the complete text is entered + // this is needed because sometimes the field value is reset while typing + // and the above character checking might not catch it due to bad timing + const currentValue = await input.getAttribute('value'); + if (currentValue !== text) { + throw new Error( + `Expected input '${selector}' to have the value '${text}' (got ${currentValue})` + ); + } }); }, diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js index 55c34615373a9..0cf20b6599acb 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js @@ -86,6 +86,7 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject } async clickDiskCol() { await find.clickByCssSelector(`[data-test-subj="${SUBJ_TABLE_SORT_DISK_COL}"] > button`); + await this.waitForTableToFinishLoading(); } async clickShardsCol() { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 6f7d693baac7f..297eb2e9b4540 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -24,6 +24,8 @@ { "path": "../plugins/global_search/tsconfig.json" }, { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../../src/plugins/telemetry/tsconfig.json" } + { "path": "../../src/plugins/telemetry/tsconfig.json" }, + { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../../src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 1d392890b27fd..79309369386cf 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -13,10 +13,7 @@ "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", "plugins/licensing/**/*", - "plugins/global_search/**/*", - "../src/plugins/usage_collection/**/*", - "../src/plugins/telemetry_collection_manager/**/*", - "../src/plugins/telemetry/**/*" + "plugins/global_search/**/*" ], "compilerOptions": { "paths": { @@ -36,6 +33,8 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" } + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../src/plugins/newsfeed/tsconfig.json" }, ] } diff --git a/yarn.lock b/yarn.lock index 44c59d0264ddf..18f868440f508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,10 +1218,10 @@ dependencies: "@elastic/apm-rum-core" "^5.7.0" -"@elastic/charts@23.2.1": - version "23.2.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-23.2.1.tgz#1f48629fe4597655a7f119fd019c4d5a2cbaf252" - integrity sha512-L2jUPAWwE0xLry6DcqcngVLCa9R32pfz5jW1fyOJRWSq1Fay2swOw4joBe8PmHpvl2s8EwWi9qWBORR1z3hUeQ== +"@elastic/charts@24.0.0": + version "24.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.0.0.tgz#7b97b00a3dc873f46f764de0f28573e236b76aa7" + integrity sha512-ZFIdHcU48Wes7eb1R+48L7xLH4p7D9oSdkoX/iuwt+znD353UhiYK9u+dbrpMXeOMtFYt7dktzVAbouHcJCZPA== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1244,13 +1244,14 @@ utility-types "^3.10.0" uuid "^3.3.2" -"@elastic/elasticsearch@7.9.1": - version "7.9.1" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.9.1.tgz#40f1c38e8f70d783851c13be78a7cb346891c15e" - integrity sha512-NfPADbm9tRK/4ohpm9+aBtJ8WPKQqQaReyBKT225pi2oKQO1IzRlfM+OPplAvbhoH1efrSj1NKk27L+4BCrzXQ== +"@elastic/elasticsearch@7.10.0-rc.1": + version "7.10.0-rc.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0-rc.1.tgz#c23fc5cbfdb40cf2ce6f9cd796b75940e8c9dc8a" + integrity sha512-STaBlEwYbT8yT3HJ+mbO1kx+Kb7Ft7Q0xG5GxZbqbAJ7PZvgGgJWwN7jUg4oKJHbTfxV3lPvFa+PaUK2TqGuYg== dependencies: debug "^4.1.1" decompress-response "^4.2.0" + hpagent "^0.1.1" ms "^2.1.1" pump "^3.0.0" secure-json-parse "^2.1.0" @@ -4584,6 +4585,21 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== +"@types/pdfkit@*": + version "0.10.6" + resolved "https://registry.yarnpkg.com/@types/pdfkit/-/pdfkit-0.10.6.tgz#9ddde7e642e6c3f1245134456a03fbc4b67dc4b5" + integrity sha512-o6R2fO/fhg392YNYahiaNGZ8CNdSKMmf5W0LvyjJ/63mLQEPTgl6M9vb4zKoHaGpsP43VimuBz+xgVevsPy8jA== + dependencies: + "@types/node" "*" + +"@types/pdfmake@^0.1.15": + version "0.1.15" + resolved "https://registry.yarnpkg.com/@types/pdfmake/-/pdfmake-0.1.15.tgz#383a8fca407612a580b82d1ca496d39001aee102" + integrity sha512-uyKefZzC1OUTKoUdY0fU9n7BjciSSYPHq2KLQmGNejeZn6Xo6hI04xhAGk368Rv9wHjKo36IGLIVIysvhrGVJQ== + dependencies: + "@types/node" "*" + "@types/pdfkit" "*" + "@types/pegjs@^0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" @@ -9804,10 +9820,10 @@ cypress-promise@^1.1.0: resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA== -cypress@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.2.0.tgz#6902efd90703242a2539f0623c6e1118aff01f95" - integrity sha512-9S2spcrpIXrQ+CQIKHsjRoLQyRc2ehB06clJXPXXp1zyOL/uZMM3Qc20ipNki4CcNwY0nBTQZffPbRpODeGYQg== +cypress@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.4.0.tgz#8833a76e91129add601f823d43c53eb512d162c5" + integrity sha512-BJR+u3DRSYMqaBS1a3l1rbh5AkMRHugbxcYYzkl+xYlO6dzcJVE8uAhghzVI/hxijCyBg1iuSe4TRp/g1PUg8Q== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" @@ -15100,6 +15116,11 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +hpagent@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2" + integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ== + html-element-map@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"