diff --git a/.bazelrc.common b/.bazelrc.common index 115c0214b1a533..3de2bceaad3a68 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -94,6 +94,12 @@ build --nolegacy_external_runfiles run --nolegacy_external_runfiles test --nolegacy_external_runfiles +# Runfiles should be enabled un order to use `exports_directories_only` in the yarn_install rule +# https://bazelbuild.github.io/rules_nodejs/Built-ins.html#yarn_install-exports_directories_only +build --enable_runfiles +run --enable_runfiles +test --enable_runfiles + # Turn on --incompatible_strict_action_env which was on by default # in Bazel 0.21.0 but turned off again in 0.22.0. Follow # https://github.com/bazelbuild/bazel/issues/7026 for more details. @@ -104,10 +110,11 @@ build --incompatible_strict_action_env run --incompatible_strict_action_env test --incompatible_strict_action_env +# DISABLED for now due to https://bazelbuild.github.io/rules_nodejs/Built-ins.html#yarn_install-exports_directories_only # Do not build runfile trees by default. If an execution strategy relies on runfile # symlink tree, the tree is created on-demand. See: https://github.com/bazelbuild/bazel/issues/6627 # and https://github.com/bazelbuild/bazel/commit/03246077f948f2790a83520e7dccc2625650e6df -build --nobuild_runfile_links +# build --nobuild_runfile_links # When running `bazel coverage` --instrument_test_targets needs to be set in order to # collect coverage information from test targets diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f2d67498130131..5fcb619af65704 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -252,6 +252,7 @@ /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security /src/plugins/spaces_oss/ @elastic/kibana-security +/src/plugins/user_setup/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security diff --git a/.node-version b/.node-version index 62df50f1eefe19..0a9f3027ffc44e 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.0 +14.17.2 diff --git a/.nvmrc b/.nvmrc index 62df50f1eefe19..0a9f3027ffc44e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.0 +14.17.2 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index ebf7bbc8488ac8..f63eb0ed8a6717 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.6.0") # we can update that rule. node_repositories( node_repositories = { - "14.17.0-darwin_amd64": ("node-v14.17.0-darwin-x64.tar.gz", "node-v14.17.0-darwin-x64", "7b210652e11d1ee25650c164cf32381895e1dcb3e0ff1d0841d8abc1f47ac73e"), - "14.17.0-linux_arm64": ("node-v14.17.0-linux-arm64.tar.xz", "node-v14.17.0-linux-arm64", "712e5575cee20570a0a56f4d4b4572cb0f2ee2f4bce49433de18be0393e7df22"), - "14.17.0-linux_s390x": ("node-v14.17.0-linux-s390x.tar.xz", "node-v14.17.0-linux-s390x", "6419372b9e9ad37e0bce188dc5740f2f060aaa44454418e462b4088a310a1c0b"), - "14.17.0-linux_amd64": ("node-v14.17.0-linux-x64.tar.xz", "node-v14.17.0-linux-x64", "494b161759a3d19c70e3172d33ce1918dd8df9ad20d29d1652a8387a84e2d308"), - "14.17.0-windows_amd64": ("node-v14.17.0-win-x64.zip", "node-v14.17.0-win-x64", "6582a7259c433e9f667dcc4ed3e5d68bc514caba2eed40e4626c8b4c7e5ecd5c"), + "14.17.2-darwin_amd64": ("node-v14.17.2-darwin-x64.tar.gz", "node-v14.17.2-darwin-x64", "e45db91fc2136202868a5eb7c6d08b0a2b75394fafdf8538f650fa945b7dee16"), + "14.17.2-linux_arm64": ("node-v14.17.2-linux-arm64.tar.xz", "node-v14.17.2-linux-arm64", "3aff08c49b8c0c3443e7a9ea9bfe607867d79e6e5ccf204a5c8f13fb92a48abd"), + "14.17.2-linux_s390x": ("node-v14.17.2-linux-s390x.tar.xz", "node-v14.17.2-linux-s390x", "76f955856626a3e596b438855fdfe438937623dc71af9a25a8466409be470877"), + "14.17.2-linux_amd64": ("node-v14.17.2-linux-x64.tar.xz", "node-v14.17.2-linux-x64", "6cf9db7349407c177b36205feec949729d0ee9db485e19b10b0b1ffca65a3a46"), + "14.17.2-windows_amd64": ("node-v14.17.2-win-x64.zip", "node-v14.17.2-win-x64", "0e27897578752865fa61546d75b20f7cd62957726caab3c03f82c57a4aef5636"), }, - node_version = "14.17.0", + node_version = "14.17.2", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], @@ -59,6 +59,7 @@ yarn_install( "//:preinstall_check.js", "//:node_modules/.yarn-integrity", ], + exports_directories_only = True, symlink_node_modules = True, quiet = False, frozen_lockfile = False, diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b4be27eee5ed2d..ffc918af925144 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -256,6 +256,10 @@ In general this plugin provides: |The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data. +|{kib-repo}blob/{branch}/src/plugins/user_setup/README.md[userSetup] +|The plugin provides UI and APIs for the interactive setup mode. + + |{kib-repo}blob/{branch}/src/plugins/vis_default_editor/README.md[visDefaultEditor] |The default editor is used in most primary visualizations, e.x. Area, Data table, Pie, etc. It acts as a container for a particular visualization and options tabs. Contains the default "Data" tab in public/components/sidebar/data_tab.tsx. diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md index 63302f50204fed..b944c9dcc02a2b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md @@ -18,5 +18,6 @@ export interface EmbeddableEditorState | --- | --- | --- | | [embeddableId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.embeddableid.md) | string | | | [originatingApp](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingapp.md) | string | | +| [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md) | string | Pass current search session id when navigating to an editor, Editors could use it continue previous search session | | [valueInput](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.valueinput.md) | EmbeddableInput | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md new file mode 100644 index 00000000000000..815055fe9f55d0 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableEditorState](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) > [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md) + +## EmbeddableEditorState.searchSessionId property + +Pass current search session id when navigating to an editor, Editors could use it continue previous search session + +Signature: + +```typescript +searchSessionId?: string; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md index 1c0b1b8bf8b462..b3e851a6d0c30b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md @@ -18,5 +18,6 @@ export interface EmbeddablePackageState | --- | --- | --- | | [embeddableId](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.embeddableid.md) | string | | | [input](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.input.md) | Optional<EmbeddableInput, 'id'> | Optional<SavedObjectEmbeddableInput, 'id'> | | +| [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.searchsessionid.md) | string | Pass current search session id when navigating to an editor, Editors could use it continue previous search session | | [type](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.type.md) | string | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.searchsessionid.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.searchsessionid.md new file mode 100644 index 00000000000000..3c515b1fb66741 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablepackagestate.searchsessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddablePackageState](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) > [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.searchsessionid.md) + +## EmbeddablePackageState.searchSessionId property + +Pass current search session id when navigating to an editor, Editors could use it continue previous search session + +Signature: + +```typescript +searchSessionId?: string; +``` 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 46934e119aee09..434f2660e7eff3 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): Observable; +interpret(ast: ExpressionAstNode, input: T): Observable>; ``` ## Parameters @@ -19,5 +19,5 @@ interpret(ast: ExpressionAstNode, input: T): Observable; Returns: -`Observable` +`Observable>` 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 30fe9f497f7eeb..edaf1c9a9ce9eb 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 @@ -26,8 +26,8 @@ export declare class Executionstring | | | [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 | | -| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | Observable<Output | ExpressionValueError> | Future that tracks result or error of this execution. | -| [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | +| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | Observable<ExecutionResult<Output | ExpressionValueError>> | Future that tracks result or error of this execution. | +| [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | ExecutionContainer<ExecutionResult<Output | ExpressionValueError>> | Dynamic state of the execution. | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md index 94f60ccee0f009..a386302a62805e 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.result.md @@ -9,5 +9,5 @@ Future that tracks result or error of this execution. Signature: ```typescript -readonly result: Observable; +readonly result: Observable>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md index 64cf81b376948a..352226da6d72ad 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): Observable; +start(input?: Input): Observable>; ``` ## Parameters @@ -22,5 +22,5 @@ start(input?: Input): Observable; Returns: -`Observable` +`Observable>` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.state.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.state.md index ca8b57b760f291..61aa0cf4c5b5dc 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.state.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.state.md @@ -9,5 +9,5 @@ Dynamic state of the execution. Signature: ```typescript -readonly state: ExecutionContainer; +readonly state: ExecutionContainer>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.getdata.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.getdata.md index dcd96cf5767bf7..852e1f58cc6f3b 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.getdata.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontract.getdata.md @@ -9,5 +9,5 @@ Returns the final output of expression, if any error happens still wraps that er Signature: ```typescript -getData: () => Promise; +getData: () => Observable>; ``` 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 f2c050bbfe0ba2..0ac776e4be2b84 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 @@ -25,7 +25,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<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. | +| [getData](./kibana-plugin-plugins-expressions-public.executioncontract.getdata.md) | | () => Observable<ExecutionResult<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. | | [inspect](./kibana-plugin-plugins-expressions-public.executioncontract.inspect.md) | | () => InspectorAdapters | Get Inspector adapters provided to all functions of expression through execution context. | | [isPending](./kibana-plugin-plugins-expressions-public.executioncontract.ispending.md) | | boolean | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md index 307e6b6bcd5c80..4eefc63d714d19 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.run.md @@ -9,7 +9,7 @@ Execute expression and return result. Signature: ```typescript -run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable>; ``` ## Parameters @@ -22,5 +22,5 @@ run(ast: string | ExpressionAstExpression, input: Input, params?: Returns: -`Observable` +`Observable>` 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 6b678fc4fbc264..9821f0f921e4d2 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 @@ -21,7 +21,7 @@ export interface ExpressionsServiceStart | [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>(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. +| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Observable<ExecutionResult<Output | ExpressionValueError>> | 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 9efca0011174cc..0838d640d54e45 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: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise; +run: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Observable>; ``` 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 4ef1225ae0d7e3..69f9d380422b66 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 | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | +| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md new file mode 100644 index 00000000000000..84c42c3f59f26d --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.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) > [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) + +## IExpressionLoaderParams.partial property + +Signature: + +```typescript +partial?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index 92ea071b23dfce..d38027753a6ff4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -18,7 +18,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | string[] | | | [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | number | | | [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | string | ExpressionAstExpression | | -| [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md) | <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void | | +| [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md) | <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters, partial?: boolean) => void | | | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | | [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | Observable<unknown> | An observable which can be used to re-run the expression without destroying the component | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md index 05ddb0b13a5bee..47559d0f7653c6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md @@ -7,5 +7,5 @@ Signature: ```typescript -onData$?: (data: TData, adapters?: TInspectorAdapters) => void; +onData$?: (data: TData, adapters?: TInspectorAdapters, partial?: boolean) => void; ``` 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 936e98be589a35..99804dd20841d5 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): Observable; +interpret(ast: ExpressionAstNode, input: T): Observable>; ``` ## Parameters @@ -19,5 +19,5 @@ interpret(ast: ExpressionAstNode, input: T): Observable; Returns: -`Observable` +`Observable>` 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 a4e324eef6674c..47963e5e5ef46f 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 @@ -26,8 +26,8 @@ export declare class Executionstring | | | [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 | | -| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | Observable<Output | ExpressionValueError> | Future that tracks result or error of this execution. | -| [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | ExecutionContainer<Output | ExpressionValueError> | Dynamic state of the execution. | +| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | Observable<ExecutionResult<Output | ExpressionValueError>> | Future that tracks result or error of this execution. | +| [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | ExecutionContainer<ExecutionResult<Output | ExpressionValueError>> | Dynamic state of the execution. | ## Methods diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md index 06cf047ac4160f..b3baac5be2fa33 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.result.md @@ -9,5 +9,5 @@ Future that tracks result or error of this execution. Signature: ```typescript -readonly result: Observable; +readonly result: Observable>; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md index dd0456ac09950e..0eef7013cb3c61 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.start.md @@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons Signature: ```typescript -start(input?: Input): Observable; +start(input?: Input): Observable>; ``` ## Parameters @@ -22,5 +22,5 @@ start(input?: Input): Observable; Returns: -`Observable` +`Observable>` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.state.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.state.md index 41e7e693a1da44..b7c26e9dee85af 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.state.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.execution.state.md @@ -9,5 +9,5 @@ Dynamic state of the execution. Signature: ```typescript -readonly state: ExecutionContainer; +readonly state: ExecutionContainer>; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md index 2ab534eac2f3a2..7b169d05dc31da 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.run.md @@ -9,7 +9,7 @@ Execute expression and return result. Signature: ```typescript -run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; +run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable>; ``` ## Parameters @@ -22,5 +22,5 @@ run(ast: string | ExpressionAstExpression, input: Input, params?: Returns: -`Observable` +`Observable>` diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index 5e6a60f019bea2..d9835b312f3ee7 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -143,3 +143,6 @@ If you are you ready to add your own data, refer to <> + +If you want to try out {ml-features} with the sample data sets, refer to +{ml-docs}/ml-getting-started.html[Getting started with {ml}]. \ No newline at end of file diff --git a/docs/maps/images/enable_filter_by_map_extent.png b/docs/maps/images/enable_filter_by_map_extent.png new file mode 100644 index 00000000000000..5132dc8f73dbef Binary files /dev/null and b/docs/maps/images/enable_filter_by_map_extent.png differ diff --git a/docs/maps/images/timeslider.gif b/docs/maps/images/timeslider.gif new file mode 100644 index 00000000000000..463adf9a9300d7 Binary files /dev/null and b/docs/maps/images/timeslider.gif differ diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index 20320c5a938c9e..0922a4cc94c222 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -11,6 +11,7 @@ Create beautiful maps from your geographical data. With **Maps**, you can: * Build maps with multiple layers and indices. +* Animate spatial temporal data. * Upload GeoJSON. * Embed your map in dashboards. * Symbolize features using data values. @@ -39,6 +40,15 @@ Use multiple layers and indices to show all your data in a single map. Show how [role="screenshot"] image::maps/images/sample_data_ecommerce.png[] +[float] +=== Animate spatial temporal data +Data comes to life with animation. Hard to detect patterns in static data pop out with movement. Use time slider to animate your data and gain deeper insights. + +This animated map uses the time slider to show Portland buses over a period of 15 minutes. The routes come alive as the bus locations update with time. + +[role="screenshot"] +image::maps/images/timeslider.gif[] + [float] === Upload GeoJSON Use **Maps** to drag and drop your GeoJSON points, lines, and polygons into Elasticsearch, and then use them as layers in your map. diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 031c7be077f520..ada7551f3e57d5 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -8,7 +8,8 @@ Layers that request data from {es} are narrowed when you submit a <> with a configured time field are narrowed by the <>. -Layers narrowed by the time filter contain the clock icon image:maps/images/clock_icon.png[] next to the layer name in the legend. +These layers contain the clock icon image:maps/images/clock_icon.png[clock icon] next to the layer name in the legend. Use the time slider to quickly select time slices within the global time filter range. +Click previous and next buttons to advance the time slice backward or forward. Click play to animate your spatial temporal data. You can create a layer that requests data from {es} from the following: @@ -35,13 +36,30 @@ image::maps/images/global_search_bar.png[] [[maps-create-filter-from-map]] === Create filters from a map -You can create two types of filters by interacting with your map: +Create filters from your map to focus in on just the data you want. *Maps* provides three ways to create filters: -* <> -* <> +* <> +* <> +* <> + +[float] +[[maps-map-extent-filter]] +==== Filter dashboard by map extent + +A map extent shows uniform data across all panels. +As you pan and zoom your map, all panels will update to only include data that is visable in your map. + +To enable filtering your dashboard by map extent: + +* Open the main menu, and then click *Dashboard*. +* Select your dashboard from the list or click *Create dashboard*. +* If your dashboard does not have a map, add a map panel. +* Click the gear icon image:maps/images/gear_icon.png[gear icon] to open the map panel menu. +* Select *More* to view all panel options. +* Select *Enable filter by map extent*. [role="screenshot"] -image::maps/images/create_spatial_filter.png[] +image::maps/images/enable_filter_by_map_extent.png[] [float] [[maps-spatial-filters]] @@ -57,10 +75,12 @@ You can create spatial filters in two ways: Spatial filters have the following properties: * *Geometry label* enables you to provide a meaningful name for your spatial filter. -* *Spatial field* specifies the geo_point or geo_shape field used to determine if a document matches the spatial relation with the specified geometry. * *Spatial relation* determines the {ref}/query-dsl-geo-shape-query.html#_spatial_relations[spatial relation operator] to use at search time. * *Action* specifies whether to apply the filter to the current view or to a drilldown action. Only available when the map is a panel in a {kibana-ref}/dashboard.html[dashboard] with {kibana-ref}/drilldowns.html[drilldowns]. +[role="screenshot"] +image::maps/images/create_spatial_filter.png[] + [float] [[maps-phrase-filter]] ==== Phrase filters diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 81f39457795035..8eea3b1ee45523 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -30,6 +30,8 @@ Kibana supports spaces in several ways. You can: The `kibana_admin` role or equivilent is required to manage **Spaces**. +TIP: Looking to support multiple tenants? See <> for more information. + [float] [[spaces-managing]] === View, create, and delete spaces diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index af67b3016454b0..516f4c66d47bbc 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -27,12 +27,12 @@ Panels display your data in charts, tables, maps, and more, which allow you to c | Display the results from machine learning anomaly detection jobs. | <> -| Display an anomaly chart from the *Anomaly Explorer. +| Display an anomaly chart from the *Anomaly Explorer*. | <> | Display a table of live streaming logs. -| <> +| <> | Add context to your panels with <>, or add dynamic filters with <>. |=== @@ -131,6 +131,38 @@ If you change your mind and want to add the panel to the *Visualize Library*: . Enter the panel title, then click *Save*. +[float] +[[add-text]] +== Add context to panels + +To provide context to your dashboard panels, add *Text* panels that display important information, instructions, images, and more. + +You create *Text* panels using GitHub-flavored Markdown text. For information about GitHub-flavored Markdown text, click *Help*. + +. From the dashboard, click *All types*, then select *Text*. + +. In the *Markdown* field, enter the text, then click *Update*. + +For example, when you enter: + +[role="screenshot"] +image::images/markdown_example_1.png[] + +The following instructions are displayed: + +[role="screenshot"] +image::images/markdown_example_2.png[] + +Or when you enter: + +[role="screenshot"] +image::images/markdown_example_3.png[] + +The following image is displayed: + +[role="screenshot"] +image::images/markdown_example_4.png[] + [float] [[edit-panels]] == Edit panels @@ -316,8 +348,6 @@ include::lens-advanced.asciidoc[] include::create-panels-with-editors.asciidoc[] -include::enhance-dashboards.asciidoc[] - -include::drilldowns.asciidoc[] +include::make-dashboards-interactive.asciidoc[] include::aggregation-reference.asciidoc[] diff --git a/docs/user/dashboard/enhance-dashboards.asciidoc b/docs/user/dashboard/enhance-dashboards.asciidoc index c999ec9b68251d..de8af34e6d32e8 100644 --- a/docs/user/dashboard/enhance-dashboards.asciidoc +++ b/docs/user/dashboard/enhance-dashboards.asciidoc @@ -8,7 +8,7 @@ To make your dashboard look the way you want, use the editing options. [[add-controls]] === Add controls -To filter the data on your dashboard in real-time, add a *Controls* panel. +To filter the data on your dashboard in real-time, add a *Controls* panel or use a map panel to <>. {kib} supports two types of *Controls*: diff --git a/docs/user/dashboard/images/drilldown_on_data_table.gif b/docs/user/dashboard/images/drilldown_on_data_table.gif new file mode 100644 index 00000000000000..926b0ff43aea63 Binary files /dev/null and b/docs/user/dashboard/images/drilldown_on_data_table.gif differ diff --git a/docs/user/dashboard/images/drilldown_on_panel.png b/docs/user/dashboard/images/drilldown_on_panel.png index 591f3280c7ecad..71b83ae457062d 100644 Binary files a/docs/user/dashboard/images/drilldown_on_panel.png and b/docs/user/dashboard/images/drilldown_on_panel.png differ diff --git a/docs/user/dashboard/make-dashboards-interactive.asciidoc b/docs/user/dashboard/make-dashboards-interactive.asciidoc new file mode 100644 index 00000000000000..3c2397d0241bb7 --- /dev/null +++ b/docs/user/dashboard/make-dashboards-interactive.asciidoc @@ -0,0 +1,241 @@ +[[role="xpack"]] +[[drilldowns]] +== Make dashboards interactive + +:keywords: administrator, analyst, concept, task, analyze, dashboard, controls, drilldowns +:description: Add interactive capabilities to your dashboard, such as controls that allow \ +you to apply dashboard-level filters, and drilldowns that allow you to navigate to other \ +dashboards and external websites. + +Add interactive capabilities to your dashboard, such as controls that allow you to apply dashboard-level filters, and drilldowns that allow you to navigate to other dashboards and external websites. + +*Controls* panels allow you to apply dashboard-level filters based on values from a list, or a range of values. + +{kib} supports two types of *Controls*: + +* *Options list* — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. +For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. + +* *Range slider* — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a +min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. ++ +[role="screenshot"] +image::images/dashboard-controls.png[] + +Panels have built-in interactive capabilities that apply filters to the dashboard data. For example, when you drag a time range or click a pie slice, a filter for the time range or pie slice is applied. Drilldowns let you customize the interactive behavior while keeping the context of the interaction. + +{kib} supports two types of drilldowns: + +* *Dashboard* — Navigates you from one dashboard to another dashboard. For example, when can create a drilldown for a *Lens* panel that navigates you from a summary dashboard to a dashboard with a filter for a specific host name. + +* *URL* — Navigates you from a dashboard to an external website. For example, a website with the specific host name as a parameter. + +++++ + + +
+++++ + +Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin]. + +[float] +[[add-controls]] +=== Add Controls panels + +Add interactive dashboard-level filter panels to your dashboard. + +. On the dashboard, click *All types*, then select *Controls*. + +. Click *Options*, then configure the following options: + +* *Update {kib} filters on each change* — When selected, all interactive inputs create filters that refresh the dashboard. When unselected, + {kib} filters are created only when you click *Apply changes*. + +* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. + +* *Pin filters for all applications* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update* + +[float] +[[dashboard-drilldowns]] +=== Create dashboard drilldowns + +Dashboard drilldowns enable you to open a dashboard from another dashboard, taking the time range, filters, and other parameters with you so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective. + +For example, if you have a dashboard that shows the logs and metrics for multiple data centers, you can create a drilldown that navigates from the dashboard that shows multiple data centers, to a dashboard that shows a single data center or server. + +[role="screenshot"] +image:images/drilldown_on_data_table.gif[Drilldown on data table that navigates to another dashboard] + +The panels you create using the following editors support dashboard drilldowns: + +* *Lens* +* *Maps* +* *TSVB* +* *Vega* +* *Aggregation-based* area chart, data table, heat map, horitizontal bar chart, line chart, pie chart, tag cloud, and vertical bar chart +* *Timelion* + +[float] +==== Create and set up the dashboards you want to connect + +Use the <> data to create a dashboard and add panels, then set a search and filter on the *[Logs] Web traffic* dashboard. + +. Add the *Sample web logs* data. + +. Create a new dashboard, click *Add from Library*, then add the following panels: + +* *[Logs] Heatmap* +* *[Logs] Host, Visits, and Bytes Table* +* *[Logs] Total Requests and Bytes* +* *[Logs] Visitors by OS* + +. Set the <> to *Last 30 days*. + +. Save the dashboard. In the *Title* field, enter `Host Overview`. + +. Open the *[Logs] Web traffic* dashboard. + +. Create a data table. + +.. In the toolbar, click *Edit*. + +.. Click *Create visualization*. + +.. From the *Chart type* dropdown, select *Table*. + +.. From the *Available fields* list, drag and drop the following fields onto the visualization builder: + +* *agent.keyword* + +* *bytes* + +* *geo.src* + +* *ip* + +.. In the editor, remove *Count of records*, then click *Save and return*. + +. On the *[Logs] Web traffic* dashboard, set a search and filter. ++ +[%hardbreaks] +Search: `extension.keyword: ("gz" or "css" or "deb")` +Filter: `geo.src: CN` + +[float] +==== Create the drilldown + +Create a drilldown that opens the *Host Overview* dashboard from the *[Logs] Web traffic* dashboard. + +. Open the panel menu for the data table you created, then select *Create drilldown*. + +. Click *Go to dashboard*. + +.. Give the drilldown a name. For example, `My Drilldown`. + +.. From the *Choose a destination dashboard* dropdown, select *Host Overview*. + +.. To use the geo.src filter, KQL query, and time filter, select *Use filters and query from origin dashboard* and *Use date range from origin dashboard*. + +.. Click *Create drilldown*. + +. Save the dashboard. + +. In the data table panel, hover over a value, click *+*, then select `My Drilldown`. ++ +[role="screenshot"] +image::images/drilldown_on_panel.png[Drilldown on data table that navigates to another dashboard] + +[float] +[[url-drilldowns]] +=== Create URL drilldowns + +URL drilldowns enable you to navigate from a dashboard to external websites. Destination URLs can be dynamic, depending on the dashboard context or user interaction with a panel. To create URL drilldowns, you add <> to a URL template, which configures the behavior of the drilldown. + +[role="screenshot"] +image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] + +Some panels support multiple interactions, also known as triggers. +The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers: + +* *Single click* — A single data point in the panel. + +* *Range selection* — A range of values in a panel. + +For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. + +The panels you create using the following editors support dashboard drilldowns: + +* *Lens* +* *Maps* +* *TSVB* +* *Aggregation-based* area chart, data table, heat map, horitizontal bar chart, line chart, pie chart, tag cloud, and vertical bar chart + +[float] +==== Create a URL drilldown + +For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown that opens Github from the dashboard panel. + +. Add the *Sample web logs* data. + +. Open the *[Logs] Web traffic* dashboard. + +. In the toolbar, click *Edit*. + +. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*. + +. Click *Go to URL*. + +.. Give the drilldown a name. For example, `Show on Github`. + +.. For the *Trigger*, select *Single click*. + +.. To navigate to the {kib} repository Github issues, enter the following in the *Enter URL* field: ++ +[source, bash] +---- +https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} +---- ++ +{kib} substitutes `{{event.value}}` with a value associated with the selected pie slice. + +.. Click *Create drilldown*. + +. Save the dashboard. + +. On the *[Logs] Visitors by OS* panel, click any chart slice, then select *Show on Github*. ++ +[role="screenshot"] +image:images/url_drilldown_popup.png[URL drilldown popup] + +. In the list of {kib} repository issues, verify that the slice value appears. ++ +[role="screenshot"] +image:images/url_drilldown_github.png[Github] + +[float] +[[manage-drilldowns]] +=== Manage drilldowns + +Make changes to your drilldowns, make a copy of your drilldowns for another panel, and delete drilldowns. + +. Open the panel menu that includes the drilldown, then click *Manage drilldowns*. + +. On the *Manage* tab, use the following options: + +* To change drilldowns, click *Edit* next to the drilldown you want to change, make your changes, then click *Save*. + +* To make a copy, click *Copy* next to the drilldown you want to change, enter the drilldown name, then click *Create drilldown*. + +* To delete a drilldown, select the drilldown you want to delete, then click *Delete*. + +include::url-drilldown.asciidoc[] diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index c62f137f985281..523a90bdf07ce4 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -6,7 +6,21 @@ The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built- When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants {kib} privileges is ineffective because `kibana_admin` has access to all the features in all spaces. -NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to {kib} segments is to grant users access to specific spaces. +[[xpack-security-multiple-tenants]] +==== Supporting multiple tenants + +There are two approaches to supporting multi-tenancy in {kib}: + +1. *Recommended:* Create a space and a limited role for each tenant, and configure each user with the appropriate role. See +<> for more details. +2. deprecated:[7.13.0,"In 8.0 and later, the `kibana.index` setting will no longer be supported."] Set up separate {kib} instances to work +with a single {es} cluster by changing the `kibana.index` setting in your `kibana.yml` file. ++ +NOTE: When using multiple {kib} instances this way, you cannot use the `kibana_admin` role to grant access. You must create custom roles +that authorize the user for each specific instance. + +Whichever approach you use, be careful when granting cluster privileges and index privileges. Both of these approaches share the same {es} +cluster, and {kib} spaces do not prevent you from granting users of two different tenants access to the same index. [role="xpack"] [[xpack-kibana-role-management]] diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc index 63b83712e3e6e1..199f138347fa02 100644 --- a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc +++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc @@ -11,7 +11,7 @@ This guide introduces you to three of {kib}'s security features: spaces, roles, [float] === Spaces -Do you have multiple teams using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help. +Do you have multiple teams or tenants using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help. Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-get-started-ref}/overview.html[monitor application performance]. diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx index 05749a0e897351..a635fab7ec8ae9 100644 --- a/examples/expressions_explorer/public/run_expressions.tsx +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -7,6 +7,7 @@ */ import React, { useState, useEffect, useMemo } from 'react'; +import { pluck } from 'rxjs/operators'; import { EuiCodeBlock, EuiFlexItem, @@ -35,7 +36,7 @@ interface Props { export function RunExpressionsExample({ expressions, inspector }: Props) { const [expression, updateExpression] = useState('markdown "## expressions explorer"'); - const [result, updateResult] = useState({}); + const [result, updateResult] = useState({}); const expressionChanged = (value: string) => { updateExpression(value); @@ -49,17 +50,13 @@ export function RunExpressionsExample({ expressions, inspector }: Props) { ); useEffect(() => { - const runExpression = async () => { - const execution = expressions.execute(expression, null, { - debug: true, - inspectorAdapters, - }); + const execution = expressions.execute(expression, null, { + debug: true, + inspectorAdapters, + }); + const subscription = execution.getData().pipe(pluck('result')).subscribe(updateResult); - const data: any = await execution.getData(); - updateResult(data); - }; - - runExpression(); + return () => subscription.unsubscribe(); }, [expression, expressions, inspectorAdapters]); return ( diff --git a/examples/locator_explorer/public/app.tsx b/examples/locator_explorer/public/app.tsx index 440e16302dff9a..8e38c097a847eb 100644 --- a/examples/locator_explorer/public/app.tsx +++ b/examples/locator_explorer/public/app.tsx @@ -19,7 +19,7 @@ import { EuiFieldText } from '@elastic/eui'; import { EuiPageHeader } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { AppMountParameters } from '../../../src/core/public'; -import { SharePluginSetup } from '../../../src/plugins/share/public'; +import { formatSearchParams, SharePluginSetup } from '../../../src/plugins/share/public'; import { HelloLocatorV1Params, HelloLocatorV2Params, @@ -34,6 +34,7 @@ interface MigratedLink { linkText: string; link: string; version: string; + params: HelloLocatorV1Params | HelloLocatorV2Params; } const ActionsExplorer = ({ share }: Props) => { @@ -93,6 +94,7 @@ const ActionsExplorer = ({ share }: Props) => { linkText: savedLink.linkText, link, version: savedLink.version, + params: savedLink.params, } as MigratedLink; }) ); @@ -157,7 +159,24 @@ const ActionsExplorer = ({ share }: Props) => { target="_blank" > {link.linkText} + {' '} + ( + + through redirect app + )
)) diff --git a/package.json b/package.json index 1cc379fb807d07..de7df7fea3d8df 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.17.0", + "node": "14.17.2", "yarn": "^1.21.1" }, "dependencies": { diff --git a/packages/kbn-i18n/GUIDELINE.md b/packages/kbn-i18n/GUIDELINE.md index 437e73bb270199..806e799bd1106f 100644 --- a/packages/kbn-i18n/GUIDELINE.md +++ b/packages/kbn-i18n/GUIDELINE.md @@ -19,6 +19,7 @@ Ids should end with: - ErrorMessage (if it's an error message), - LinkText (if it's `` tag), - ToggleSwitch and etc. +- `.markdown` (if it's markdown) There is one more complex case, when we have to divide a single expression into different labels. @@ -110,7 +111,7 @@ Currently, we support the following AngluarJS `i18n` tools, but they will be rem ### Naming convention The message ids chosen for message keys should always be descriptive of the string, and its role in the interface (button label, title, etc.). Think of them as long variable names. When you have to change a message id, adding a progressive number to the existing key should always be used as a last resort. -Here's a rule of id maning: +Here's a rule of id naming: `{plugin}.{area}.[{sub-area}].{element}` @@ -138,6 +139,7 @@ Here's a rule of id maning: 'kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch' 'kbn.management.editIndexPattern.wrongTypeErrorMessage' 'kbn.management.editIndexPattern.scripted.table.nameDescription' + 'xpack.lens.formulaDocumentation.filterRatioDescription.markdown' ``` - For complex messages, which are divided into several parts, use the following approach: @@ -192,6 +194,7 @@ Each message id should end with a type of the message. | tooltip | `kbn.management.editIndexPattern.removeTooltip` | | error message | `kbn.management.createIndexPattern.step.invalidCharactersErrorMessage` | | toggleSwitch | `kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch` | +| markdown | `xpack.lens.formulaDocumentation.filterRatioDescription.markdown` | For example: @@ -281,6 +284,27 @@ For example: /> ``` +- for markdown + ```js + import { Markdown } from '@elastic/eui'; + + + ``` + ### Variety of `values` - Variables @@ -372,6 +396,82 @@ Here is an example of message translation depending on a plural category: When `conflictFieldsLength` equals 1, the result string will be `"A field is defined as several types (string, integer, etc) across the indices that match this pattern."`. In cases when `conflictFieldsLength` has value of 2 or more, the result string - `"2 fields are defined as several types (string, integer, etc) across the indices that match this pattern."`. +### Text with markdown + +There is some support for using markdown and you can use any of the following syntax: + +#### Headers + +```md +# This is an

tag +## This is an

tag +###### This is an

tag +``` + +#### Emphasis + +```md +*This text will be italic* +_This will also be italic_ + +**This text will be bold** +__This will also be bold__ + +_You **can** combine them_ +``` + +#### Lists + ##### Unordered + +```md +* Item 1 +* Item 2 + * Item 2a + * Item 2b +``` + ##### Ordered + +```md +1. Item 1 +1. Item 2 +1. Item 3 + 1. Item 3a + 1. Item 3b +``` +#### Images + +```md +![Github Logo](/images/logo.png) +Format: ![Alt Text](url) +``` + +#### Links + +```md +http://github.com - automatic! +[GitHub](http://github.com) +``` + +#### Blockquotes + +```md +As Kanye West said: + +> We're living the future so +> the present is our past. +``` +#### Code Blocks + +```md +var a = 13; +``` + +#### Inline code + +```md +I think you should use an +`` element here instead +``` ### Splitting Splitting sentences into several keys often inadvertently presumes a grammar, a sentence structure, and such composite strings are often very difficult to translate. @@ -385,6 +485,12 @@ Splitting sentences into several keys often inadvertently presumes a grammar, a If this group of sentences is separated it’s possible that the context of the `'it'` in `'close it'` will be lost. +### Large paragraphs + +Try to avoid using large paragraphs of text. They are difficult to maintain and often need small changes when the information becomes out of date. + +If you have no other choice, you can split paragraphs into a _few_ i18n chunks. Chunks should be split at logical points to ensure they contain enough context to be intelligible on their own. + ### Unit tests #### How to test `FormattedMessage` and `i18n.translate()` components. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6627b644daec76..2c7f194d7da985 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -112,3 +112,4 @@ pageLoadAssetSize: visTypePie: 35583 expressionRevealImage: 25675 cases: 144442 + userSetup: 18532 diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6bb714e9138383..f215c86d9d507a 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -249,7 +249,7 @@ export class DocLinksService { customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, - outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-roc`, + outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-class-aucroc`, }, diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 8db6a0e8a8c7fd..638b1c83e9dc65 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -40,6 +40,7 @@ export function DashboardApp({ embeddable, onAppLeave, uiSettings, + data, } = useKibana().services; const kbnUrlStateStorage = useMemo( @@ -98,6 +99,13 @@ export function DashboardApp({ ]); }, [chrome, dashboardState.title, dashboardState.viewMode, redirectTo, savedDashboardId]); + // clear search session when leaving dashboard route + useEffect(() => { + return () => { + data.search.session.clear(); + }; + }, [data.search.session]); + return ( <> {isCompleteDashboardAppState(dashboardAppState) && ( diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index e77353000ced46..cb40b305428698 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -260,6 +260,7 @@ export async function mountApp({ } render(app, element); return () => { + dataStart.search.session.clear(); unlistenParentHistory(); unmountComponentAtNode(element); appUnMounted(); diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index cb8c5ac5745e4c..8b895d739e2d11 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -64,6 +64,11 @@ export const buildDashboardContainer = async ({ getLatestDashboardState, canStoreSearchSession: dashboardCapabilities.storeSearchSession, }); + + if (incomingEmbeddable?.searchSessionId) { + session.continue(incomingEmbeddable?.searchSessionId); + } + const searchSessionIdFromURL = getSearchSessionIdFromURL(history); if (searchSessionIdFromURL) { session.restore(searchSessionIdFromURL); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 5557bf25d9d852..7f72c77009cb9a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -87,11 +87,6 @@ export const DashboardListing = ({ }; }, [title, savedObjectsClient, redirectTo, data.query, kbnUrlStateStorage]); - // clear dangling session because they are not required here - useEffect(() => { - data.search.session.clear(); - }, [data.search.session]); - const hideWriteControls = dashboardCapabilities.hideWriteControls; const listingLimit = savedObjects.settings.getListingLimit(); const defaultFilter = title ? `"${title}"` : ''; diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index e5f89bd6a8e909..dab74373efef5f 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -204,10 +204,11 @@ export function DashboardTopNav({ path, state: { originatingApp: DashboardConstants.DASHBOARDS_ID, + searchSessionId: data.search.session.getSessionId(), }, }); }, - [trackUiMetric, stateTransferService] + [stateTransferService, data.search.session, trackUiMetric] ); const clearAddPanel = useCallback(() => { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 35094fac1cc0f1..66d81d058fc77e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2783,7 +2783,7 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:62:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index 18d32463864e36..dee0216530205f 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -47,5 +47,6 @@ export function getSessionServiceMock(): jest.Mocked { isSessionStorageReady: jest.fn(() => true), getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })), hasAccess: jest.fn(() => true), + continue: jest.fn(), }; } diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 7f388a29cd454e..c2c4d1540c387f 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -98,12 +98,12 @@ describe('Session service', () => { expect(nowProvider.reset).toHaveBeenCalled(); }); - it("Can clear other apps' session", async () => { + it("Can't clear other apps' session", async () => { sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); currentAppId$.next('change'); sessionService.clear(); - expect(sessionService.getSessionId()).toBeUndefined(); + expect(sessionService.getSessionId()).not.toBeUndefined(); }); it("Can start a new session in case there is other apps' stale session", async () => { @@ -161,6 +161,72 @@ describe('Session service', () => { }); }); + it('Can continue previous session from another app', async () => { + sessionService.start(); + const sessionId = sessionService.getSessionId(); + + sessionService.clear(); + currentAppId$.next('change'); + sessionService.continue(sessionId!); + + expect(sessionService.getSessionId()).toBe(sessionId); + }); + + it('Calling clear() more than once still allows previous session from another app to continue', async () => { + sessionService.start(); + const sessionId = sessionService.getSessionId(); + + sessionService.clear(); + sessionService.clear(); + + currentAppId$.next('change'); + sessionService.continue(sessionId!); + + expect(sessionService.getSessionId()).toBe(sessionId); + }); + + it('Continue drops storage configuration', () => { + sessionService.start(); + const sessionId = sessionService.getSessionId(); + + sessionService.enableStorage({ + getName: async () => 'Name', + getUrlGeneratorData: async () => ({ + urlGeneratorId: 'id', + initialState: {}, + restoreState: {}, + }), + }); + + expect(sessionService.isSessionStorageReady()).toBe(true); + + sessionService.clear(); + + sessionService.continue(sessionId!); + + expect(sessionService.isSessionStorageReady()).toBe(false); + }); + + // it might be that search requests finish after the session is cleared and before it was continued, + // to avoid "infinite loading" state after we continue the session we have to drop pending searches + it('Continue drops client side loading state', async () => { + const sessionId = sessionService.start(); + + sessionService.trackSearch({ abort: () => {} }); + expect(state$.getValue()).toBe(SearchSessionState.Loading); + + sessionService.clear(); // even allow to call clear multiple times + + expect(state$.getValue()).toBe(SearchSessionState.None); + + sessionService.continue(sessionId!); + expect(sessionService.getSessionId()).toBe(sessionId); + + // the original search was never `untracked`, + // but we still consider this a completed session until new search fire + expect(state$.getValue()).toBe(SearchSessionState.Completed); + }); + test('getSearchOptions infers isRestore & isStored from state', async () => { const sessionId = sessionService.start(); const someOtherId = 'some-other-id'; diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 629d76b07d7caf..32cd620a2adb2c 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -20,6 +20,7 @@ import { ConfigSchema } from '../../../config'; import { createSessionStateContainer, SearchSessionState, + SessionStateInternal, SessionMeta, SessionStateContainer, } from './search_session_state'; @@ -35,6 +36,11 @@ export interface TrackSearchDescriptor { abort: () => void; } +/** + * Represents a search session state in {@link SessionService} in any given moment of time + */ +export type SessionSnapshot = SessionStateInternal; + /** * Provide info about current search session to be stored in the Search Session saved object */ @@ -88,6 +94,13 @@ export class SessionService { private toastService?: ToastService; + /** + * Holds snapshot of last cleared session so that it can be continued + * Can be used to re-use a session between apps + * @private + */ + private lastSessionSnapshot?: SessionSnapshot; + constructor( initializerContext: PluginInitializerContext, getStartServices: StartServicesAccessor, @@ -128,6 +141,21 @@ export class SessionService { this.subscription.add( coreStart.application.currentAppId$.subscribe((newAppName) => { this.currentApp = newAppName; + if (!this.getSessionId()) return; + + // Apps required to clean up their sessions before unmounting + // Make sure that apps don't leave sessions open by throwing an error in DEV mode + const message = `Application '${ + this.state.get().appName + }' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + coreStart.fatalErrors.add(message); + } else { + // this should never happen in prod because should be caught in dev mode + // in case this happen we don't want to throw fatal error, as most likely possible bugs are not that critical + // eslint-disable-next-line no-console + console.warn(message); + } }) ); }); @@ -158,6 +186,7 @@ export class SessionService { public destroy() { this.subscription.unsubscribe(); this.clear(); + this.lastSessionSnapshot = undefined; } /** @@ -198,7 +227,9 @@ export class SessionService { */ public start() { if (!this.currentApp) throw new Error('this.currentApp is missing'); + this.state.transitions.start({ appName: this.currentApp }); + return this.getSessionId()!; } @@ -211,10 +242,52 @@ export class SessionService { this.refreshSearchSessionSavedObject(); } + /** + * Continue previous search session + * Can be used to share a running search session between different apps, so they can reuse search cache + * + * This is different from {@link restore} as it reuses search session state and search results held in client memory instead of restoring search results from elasticsearch + * @param sessionId + */ + public continue(sessionId: string) { + if (this.lastSessionSnapshot?.sessionId === sessionId) { + this.state.set({ + ...this.lastSessionSnapshot, + // have to change a name, so that current app can cancel a session that it continues + appName: this.currentApp, + // also have to drop all pending searches which are used to derive client side state of search session indicator, + // if we weren't dropping this searches, then we would get into "infinite loading" state when continuing a session that was cleared with pending searches + // possible solution to this problem is to refactor session service to support multiple sessions + pendingSearches: [], + }); + this.lastSessionSnapshot = undefined; + } else { + // eslint-disable-next-line no-console + console.warn( + `Continue search session: last known search session id: "${this.lastSessionSnapshot?.sessionId}", but received ${sessionId}` + ); + } + } + /** * Cleans up current state */ public clear() { + // make sure apps can't clear other apps' sessions + const currentSessionApp = this.state.get().appName; + if (currentSessionApp && currentSessionApp !== this.currentApp) { + // eslint-disable-next-line no-console + console.warn( + `Skip clearing session "${this.getSessionId()}" because it belongs to a different app. current: "${ + this.currentApp + }", owner: "${currentSessionApp}"` + ); + return; + } + + if (this.getSessionId()) { + this.lastSessionSnapshot = this.state.get(); + } this.state.transitions.clear(); this.searchSessionInfoProvider = undefined; this.searchSessionIndicatorUiConfig = undefined; diff --git a/src/plugins/data/server/autocomplete/terms_agg.test.ts b/src/plugins/data/server/autocomplete/terms_agg.test.ts index e4652c2c422e22..ae991e289a7159 100644 --- a/src/plugins/data/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/data/server/autocomplete/terms_agg.test.ts @@ -32,6 +32,8 @@ const mockResponse = { }, } as ApiResponse>; +jest.mock('../index_patterns'); + describe('terms agg suggestions', () => { beforeEach(() => { const requestHandlerContext = coreMock.createRequestHandlerContext(); @@ -86,4 +88,50 @@ describe('terms agg suggestions', () => { ] `); }); + + it('calls the _search API with a terms agg and fallback to fieldName when field is null', async () => { + const result = await termsAggSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [] + ); + + const [[args]] = esClientMock.search.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "suggestions": Object { + "terms": Object { + "execution_hint": "map", + "field": "fieldName", + "include": "query.*", + "shard_size": 10, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [], + }, + }, + "size": 0, + "terminate_after": 98430, + "timeout": "4513ms", + }, + "index": "index", + } + `); + expect(result).toMatchInlineSnapshot(` + Array [ + "whoa", + "amazing", + ] + `); + }); }); diff --git a/src/plugins/data/server/autocomplete/terms_enum.test.ts b/src/plugins/data/server/autocomplete/terms_enum.test.ts index be8f179db29c05..41eaf3f4032ab0 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.test.ts @@ -22,6 +22,8 @@ const mockResponse = { body: { terms: ['whoa', 'amazing'] }, }; +jest.mock('../index_patterns'); + describe('_terms_enum suggestions', () => { beforeEach(() => { const requestHandlerContext = coreMock.createRequestHandlerContext(); @@ -71,4 +73,45 @@ describe('_terms_enum suggestions', () => { `); expect(result).toEqual(mockResponse.body.terms); }); + + it('calls the _terms_enum API and fallback to fieldName when field is null', async () => { + const result = await termsEnumSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [] + ); + + const [[args]] = esClientMock.transport.request.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "field": "fieldName", + "index_filter": Object { + "bool": Object { + "must": Array [ + Object { + "terms": Object { + "_tier": Array [ + "data_hot", + "data_warm", + "data_content", + ], + }, + }, + ], + }, + }, + "string": "query", + }, + "method": "POST", + "path": "/index/_terms_enum", + } + `); + expect(result).toEqual(mockResponse.body.terms); + }); }); diff --git a/src/plugins/data/server/autocomplete/terms_enum.ts b/src/plugins/data/server/autocomplete/terms_enum.ts index c2452b0a099d04..40329586a36218 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.ts @@ -36,7 +36,7 @@ export async function termsEnumSuggestions( method: 'POST', path: encodeURI(`/${index}/_terms_enum`), body: { - field: field?.name ?? field, + field: field?.name ?? fieldName, string: query, index_filter: { bool: { diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 058fd832e15db9..ea90307ef57a10 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -111,6 +111,7 @@ export class EditPanelAction implements Action { originatingApp: this.currentAppId, valueInput: byValueMode ? this.getExplicitInput({ embeddable }) : undefined, embeddableId: embeddable.id, + searchSessionId: embeddable.getInput().searchSessionId, }; return { app, path, state }; } diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 5e5ef9c360a647..98cf6e70284cdd 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -19,6 +19,12 @@ export interface EmbeddableEditorState { originatingApp: string; embeddableId?: string; valueInput?: EmbeddableInput; + + /** + * Pass current search session id when navigating to an editor, + * Editors could use it continue previous search session + */ + searchSessionId?: string; } export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { @@ -35,6 +41,12 @@ export interface EmbeddablePackageState { type: string; input: Optional | Optional; embeddableId?: string; + + /** + * Pass current search session id when navigating to an editor, + * Editors could use it continue previous search session + */ + searchSessionId?: string; } export function isEmbeddablePackageState(state: unknown): state is EmbeddablePackageState { diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 98c48dbd848b00..a810b1f48a07ca 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -365,6 +365,7 @@ export interface EmbeddableEditorState { embeddableId?: string; // (undocumented) originatingApp: string; + searchSessionId?: string; // (undocumented) valueInput?: EmbeddableInput; } @@ -467,6 +468,7 @@ export interface EmbeddablePackageState { embeddableId?: string; // (undocumented) input: Optional | Optional; + searchSessionId?: string; // (undocumented) type: string; } diff --git a/src/plugins/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts index 514086e9b19eee..798558ba7ffb6e 100644 --- a/src/plugins/expressions/common/execution/execution.abortion.test.ts +++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { waitFor } from '@testing-library/react'; import { Execution } from './execution'; import { parseExpression } from '../ast'; @@ -40,9 +39,9 @@ describe('Execution abortion tests', () => { execution.start(); execution.cancel(); - const result = await execution.result.pipe(first()).toPromise(); + const result = await execution.result.toPromise(); - expect(result).toMatchObject({ + expect(result).toHaveProperty('result', { type: 'error', error: { message: 'The expression was aborted.', @@ -58,9 +57,9 @@ describe('Execution abortion tests', () => { jest.advanceTimersByTime(100); execution.cancel(); - const result = await execution.result.pipe(first()).toPromise(); + const result = await execution.result.toPromise(); - expect(result).toMatchObject({ + expect(result).toHaveProperty('result', { type: 'error', error: { message: 'The expression was aborted.', @@ -76,7 +75,7 @@ describe('Execution abortion tests', () => { execution.start(); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); execution.cancel(); @@ -136,7 +135,7 @@ describe('Execution abortion tests', () => { await waitFor(() => expect(started).toHaveBeenCalledTimes(1)); execution.cancel(); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toMatchObject({ type: 'error', error: { diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index feff425cc48edd..8c6f457105d426 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -7,7 +7,7 @@ */ import { of } from 'rxjs'; -import { first, scan } from 'rxjs/operators'; +import { scan } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; import { Execution } from './execution'; import { parseExpression, ExpressionAstExpression } from '../ast'; @@ -45,7 +45,7 @@ const run = async ( ) => { const execution = createExecution(expression, context); execution.start(input); - return await execution.result.pipe(first()).toPromise(); + return await execution.result.toPromise(); }; let testScheduler: TestScheduler; @@ -84,7 +84,7 @@ describe('Execution', () => { /* eslint-enable no-console */ execution.start(123); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toBe(123); expect(spy).toHaveBeenCalledTimes(1); @@ -102,7 +102,7 @@ describe('Execution', () => { value: -1, }); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -117,7 +117,7 @@ describe('Execution', () => { value: 0, }); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -131,7 +131,7 @@ describe('Execution', () => { // Below 1 is cast to { type: 'num', value: 1 }. execution.start(1); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -143,7 +143,7 @@ describe('Execution', () => { const execution = createExecution('add val=1'); execution.start(Promise.resolve(1)); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -155,7 +155,7 @@ describe('Execution', () => { const execution = createExecution('add val=1'); execution.start(of(1)); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -167,14 +167,14 @@ describe('Execution', () => { const execution = createExecution('add val=1'); testScheduler.run(({ cold, expectObservable }) => { - const input = cold(' -a--b-c-', { a: 1, b: 2, c: 3 }); + const input = cold(' -a--b-c|', { a: 1, b: 2, c: 3 }); const subscription = ' ---^---!'; - const expected = ' ---ab-c-'; + const expected = ' ---ab-c|'; expectObservable(execution.start(input), subscription).toBe(expected, { - a: { type: 'num', value: 2 }, - b: { type: 'num', value: 3 }, - c: { type: 'num', value: 4 }, + a: { partial: false, result: { type: 'num', value: 2 } }, + b: { partial: false, result: { type: 'num', value: 3 } }, + c: { partial: false, result: { type: 'num', value: 4 } }, }); }); }); @@ -187,21 +187,21 @@ describe('Execution', () => { const expected = ' -a-#'; expectObservable(execution.start(input)).toBe(expected, { - a: { type: 'num', value: 2 }, + a: { partial: false, result: { type: 'num', value: 2 } }, }); }); }); - test('does not complete when input completes', () => { + test('completes when input completes', () => { const execution = createExecution('add val=1'); testScheduler.run(({ cold, expectObservable }) => { const input = cold('-a-b|', { a: 1, b: 2 }); - const expected = ' -a-b-'; + const expected = ' -a-b|'; expectObservable(execution.start(input)).toBe(expected, { - a: { type: 'num', value: 2 }, - b: { type: 'num', value: 3 }, + a: expect.objectContaining({ result: { type: 'num', value: 2 } }), + b: expect.objectContaining({ result: { type: 'num', value: 3 } }), }); }); }); @@ -216,9 +216,9 @@ describe('Execution', () => { const input = items.pipe(scan((result, value) => [...result, value], new Array())); expectObservable(execution.start(input), subscription).toBe(expected, { - a: { type: 'num', value: 1 }, - b: { type: 'num', value: 3 }, - c: { type: 'num', value: 6 }, + a: { partial: false, result: { type: 'num', value: 1 } }, + b: { partial: false, result: { type: 'num', value: 3 } }, + c: { partial: false, result: { type: 'num', value: 6 } }, }); }); }); @@ -263,44 +263,51 @@ describe('Execution', () => { describe('execution context', () => { test('context.variables is an object', async () => { const { result } = (await run('introspectContext key="variables"')) as any; - expect(typeof result).toBe('object'); + + expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.types is an object', async () => { const { result } = (await run('introspectContext key="types"')) as any; - expect(typeof result).toBe('object'); + + expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.abortSignal is an object', async () => { const { result } = (await run('introspectContext key="abortSignal"')) as any; - expect(typeof result).toBe('object'); + + expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.inspectorAdapters is an object', async () => { const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; - expect(typeof result).toBe('object'); + + expect(result).toHaveProperty('result', expect.any(Object)); }); test('context.getKibanaRequest is a function if provided', async () => { const { result } = (await run('introspectContext key="getKibanaRequest"', { kibanaRequest: {}, })) as any; - expect(typeof result).toBe('function'); + + expect(result).toHaveProperty('result', expect.any(Function)); }); test('context.getKibanaRequest is undefined if not provided', async () => { const { result } = (await run('introspectContext key="getKibanaRequest"')) as any; - expect(typeof result).toBe('undefined'); + + expect(result).toHaveProperty('result', undefined); }); test('unknown context key is undefined', async () => { const { result } = (await run('introspectContext key="foo"')) as any; - expect(typeof result).toBe('undefined'); + + expect(result).toHaveProperty('result', undefined); }); test('can set context variables', async () => { const variables = { foo: 'bar' }; - const result = await run('var name="foo"', { variables }); + const { result } = await run('var name="foo"', { variables }); expect(result).toBe('bar'); }); }); @@ -308,10 +315,13 @@ describe('Execution', () => { describe('inspector adapters', () => { test('by default, "tables" and "requests" inspector adapters are available', async () => { const { result } = (await run('introspectContext key="inspectorAdapters"')) as any; - expect(result).toMatchObject({ - tables: expect.any(Object), - requests: expect.any(Object), - }); + expect(result).toHaveProperty( + 'result', + expect.objectContaining({ + tables: expect.any(Object), + requests: expect.any(Object), + }) + ); }); test('can set custom inspector adapters', async () => { @@ -319,7 +329,7 @@ describe('Execution', () => { const { result } = (await run('introspectContext key="inspectorAdapters"', { inspectorAdapters, })) as any; - expect(result).toBe(inspectorAdapters); + expect(result).toHaveProperty('result', inspectorAdapters); }); test('can access custom inspector adapters on Execution object', async () => { @@ -335,8 +345,7 @@ describe('Execution', () => { test('context has abortSignal object', async () => { const { result } = (await run('introspectContext key="abortSignal"')) as any; - expect(typeof result).toBe('object'); - expect((result as AbortSignal).aborted).toBe(false); + expect(result).toHaveProperty('result.aborted', false); }); }); @@ -348,7 +357,7 @@ describe('Execution', () => { value: 0, }); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -357,8 +366,8 @@ describe('Execution', () => { }); test('can execute async functions', async () => { - const res = await run('sleep 10 | sleep 10'); - expect(res).toBe(null); + const { result } = await run('sleep 10 | sleep 10'); + expect(result).toBe(null); }); test('result is undefined until execution completes', async () => { @@ -374,7 +383,7 @@ describe('Execution', () => { jest.advanceTimersByTime(10); await new Promise(process.nextTick); - expect(execution.state.get().result).toBe(null); + expect(execution.state.get().result).toHaveProperty('result', null); jest.useRealTimers(); }); @@ -382,7 +391,7 @@ describe('Execution', () => { test('handles functions returning observables', () => { testScheduler.run(({ cold, expectObservable }) => { const arg = cold(' -a-b-c|', { a: 1, b: 2, c: 3 }); - const expected = ' -a-b-c-'; + const expected = ' -a-b-c|'; const observable: ExpressionFunctionDefinition<'observable', any, {}, any> = { name: 'observable', args: {}, @@ -394,14 +403,18 @@ describe('Execution', () => { const result = executor.run('observable', null, {}); - expectObservable(result).toBe(expected, { a: 1, b: 2, c: 3 }); + expectObservable(result).toBe(expected, { + a: { result: 1, partial: true }, + b: { result: 2, partial: true }, + c: { result: 3, partial: false }, + }); }); }); }); describe('when function throws', () => { test('error is reported in output object', async () => { - const result = await run('error "foobar"'); + const { result } = await run('error "foobar"'); expect(result).toMatchObject({ type: 'error', @@ -409,7 +422,7 @@ describe('Execution', () => { }); test('error message is prefixed with function name', async () => { - const result = await run('error "foobar"'); + const { result } = await run('error "foobar"'); expect(result).toMatchObject({ error: { @@ -419,7 +432,7 @@ describe('Execution', () => { }); test('returns error of the first function that throws', async () => { - const result = await run('error "foo" | error "bar"'); + const { result } = await run('error "foo" | error "bar"'); expect(result).toMatchObject({ error: { @@ -432,15 +445,18 @@ describe('Execution', () => { const execution = await createExecution('error "foo"'); execution.start(null); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toMatchObject({ type: 'error', }); expect(execution.state.get().state).toBe('result'); - expect(execution.state.get().result).toMatchObject({ - type: 'error', - }); + expect(execution.state.get().result).toHaveProperty( + 'result', + expect.objectContaining({ + type: 'error', + }) + ); }); test('does not execute remaining functions in pipeline', async () => { @@ -453,7 +469,7 @@ describe('Execution', () => { const executor = createUnitTestExecutor(); executor.registerFunction(spy); - await executor.run('error "..." | spy', null).pipe(first()).toPromise(); + await executor.run('error "..." | spy', null).toPromise(); expect(spy.fn).toHaveBeenCalledTimes(0); }); @@ -483,21 +499,21 @@ describe('Execution', () => { test('execution state is "result" when execution successfully completes', async () => { const execution = createExecution('sleep 1'); execution.start(null); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); expect(execution.state.get().state).toBe('result'); }); test('execution state is "result" when execution successfully completes - 2', async () => { const execution = createExecution('var foo'); execution.start(null); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); expect(execution.state.get().state).toBe('result'); }); }); describe('sub-expressions', () => { test('executes sub-expressions', async () => { - const result = await run('add val={add 5 | access "value"}', {}, null); + const { result } = await run('add val={add 5 | access "value"}', {}, null); expect(result).toMatchObject({ type: 'num', @@ -506,7 +522,7 @@ describe('Execution', () => { }); test('can use global variables', async () => { - const result = await run( + const { result } = await run( 'add val={var foo}', { variables: { @@ -523,7 +539,7 @@ describe('Execution', () => { }); test('can modify global variables', async () => { - const result = await run( + const { result } = await run( 'add val={var_set name=foo value=66 | var bar} | var foo', { variables: { @@ -547,18 +563,20 @@ describe('Execution', () => { const executor = createUnitTestExecutor(); executor.registerFunction(observable); - expect( - executor.run('add val={observable}', 1, {}).pipe(first()).toPromise() - ).resolves.toEqual({ - type: 'num', - value: 2, - }); + expect(executor.run('add val={observable}', 1, {}).toPromise()).resolves.toEqual( + expect.objectContaining({ + result: { + type: 'num', + value: 2, + }, + }) + ); }); test('supports observables in arguments emitting multiple values', () => { testScheduler.run(({ cold, expectObservable }) => { - const arg = cold('-a-b-c-', { a: 1, b: 2, c: 3 }); - const expected = '-a-b-c-'; + const arg = cold('-a-b-c|', { a: 1, b: 2, c: 3 }); + const expected = '-a-b-c|'; const observable = { name: 'observable', args: {}, @@ -571,18 +589,18 @@ describe('Execution', () => { const result = executor.run('add val={observable}', 1, {}); expectObservable(result).toBe(expected, { - a: { type: 'num', value: 2 }, - b: { type: 'num', value: 3 }, - c: { type: 'num', value: 4 }, + a: { partial: true, result: { type: 'num', value: 2 } }, + b: { partial: true, result: { type: 'num', value: 3 } }, + c: { partial: false, result: { type: 'num', value: 4 } }, }); }); }); test('combines multiple observables in arguments', () => { testScheduler.run(({ cold, expectObservable }) => { - const arg1 = cold('--ab-c-', { a: 0, b: 2, c: 4 }); - const arg2 = cold('-a--bc-', { a: 1, b: 3, c: 5 }); - const expected = ' --abc(de)-'; + const arg1 = cold('--ab-c---|', { a: 0, b: 2, c: 4 }); + const arg2 = cold('-a--bc---|', { a: 1, b: 3, c: 5 }); + const expected = ' --abc(de)|'; const observable1 = { name: 'observable1', args: {}, @@ -612,32 +630,11 @@ describe('Execution', () => { const result = executor.run('max val1={observable1} val2={observable2}', {}); expectObservable(result).toBe(expected, { - a: { type: 'num', value: 1 }, - b: { type: 'num', value: 2 }, - c: { type: 'num', value: 3 }, - d: { type: 'num', value: 4 }, - e: { type: 'num', value: 5 }, - }); - }); - }); - - test('does not complete when an argument completes', () => { - testScheduler.run(({ cold, expectObservable }) => { - const arg = cold('-a|', { a: 1 }); - const expected = '-a-'; - const observable = { - name: 'observable', - args: {}, - help: '', - fn: () => arg, - }; - const executor = createUnitTestExecutor(); - executor.registerFunction(observable); - - const result = executor.run('add val={observable}', 1, {}); - - expectObservable(result).toBe(expected, { - a: { type: 'num', value: 2 }, + a: { partial: true, result: { type: 'num', value: 1 } }, + b: { partial: true, result: { type: 'num', value: 2 } }, + c: { partial: true, result: { type: 'num', value: 3 } }, + d: { partial: true, result: { type: 'num', value: 4 } }, + e: { partial: false, result: { type: 'num', value: 5 } }, }); }); }); @@ -645,7 +642,7 @@ describe('Execution', () => { test('handles error in observable arguments', () => { testScheduler.run(({ cold, expectObservable }) => { const arg = cold('-a-#', { a: 1 }, new Error('some error')); - const expected = '-a-b'; + const expected = '-a-(b|)'; const observable = { name: 'observable', args: {}, @@ -658,13 +655,15 @@ describe('Execution', () => { const result = executor.run('add val={observable}', 1, {}); expectObservable(result).toBe(expected, { - a: { type: 'num', value: 2 }, - b: { - error: expect.objectContaining({ - message: '[add] > [observable] > some error', - }), - type: 'error', - }, + a: expect.objectContaining({ result: { type: 'num', value: 2 } }), + b: expect.objectContaining({ + result: { + error: expect.objectContaining({ + message: '[add] > [observable] > some error', + }), + type: 'error', + }, + }), }); }); }); @@ -685,7 +684,7 @@ describe('Execution', () => { }; const executor = createUnitTestExecutor(); executor.registerFunction(requiredArg); - const result = await executor.run('requiredArg', null, {}).pipe(first()).toPromise(); + const { result } = await executor.run('requiredArg', null, {}).toPromise(); expect(result).toMatchObject({ type: 'error', @@ -696,7 +695,7 @@ describe('Execution', () => { }); test('when required argument is missing and has alias, returns error', async () => { - const result = await run('var_set', {}); + const { result } = await run('var_set', {}); expect(result).toMatchObject({ type: 'error', @@ -711,7 +710,7 @@ describe('Execution', () => { test('can execute expression in debug mode', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -726,7 +725,7 @@ describe('Execution', () => { true ); execution.start(0); - const result = await execution.result.pipe(first()).toPromise(); + const { result } = await execution.result.toPromise(); expect(result).toEqual({ type: 'num', @@ -738,7 +737,7 @@ describe('Execution', () => { test('sets "success" flag on all functions to true', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); for (const node of execution.state.get().ast.chain) { expect(node.debug?.success).toBe(true); @@ -748,7 +747,7 @@ describe('Execution', () => { test('stores "fn" reference to the function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); for (const node of execution.state.get().ast.chain) { expect(node.debug?.fn).toBe('add'); @@ -758,7 +757,7 @@ describe('Execution', () => { test('saves duration it took to execute each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); for (const node of execution.state.get().ast.chain) { expect(typeof node.debug?.duration).toBe('number'); @@ -770,7 +769,7 @@ describe('Execution', () => { test('adds .debug field in expression AST on each executed function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); for (const node of execution.state.get().ast.chain) { expect(typeof node.debug).toBe('object'); @@ -781,7 +780,7 @@ describe('Execution', () => { test('stores input of each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const { chain } = execution.state.get().ast; @@ -799,7 +798,7 @@ describe('Execution', () => { test('stores output of each function', async () => { const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const { chain } = execution.state.get().ast; @@ -824,7 +823,7 @@ describe('Execution', () => { true ); execution.start(-1); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const { chain } = execution.state.get().ast; @@ -847,7 +846,7 @@ describe('Execution', () => { true ); execution.start(0); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const { chain } = execution.state.get().ast.chain[0].arguments .val[0] as ExpressionAstExpression; @@ -882,7 +881,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const node1 = execution.state.get().ast.chain[0]; const node2 = execution.state.get().ast.chain[1]; @@ -900,7 +899,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const node2 = execution.state.get().ast.chain[1]; @@ -921,7 +920,7 @@ describe('Execution', () => { params: { debug: true }, }); execution.start(0); - await execution.result.pipe(first()).toPromise(); + await execution.result.toPromise(); const node2 = execution.state.get().ast.chain[1]; diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index b70f261ea4b201..47209c348257e6 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -20,7 +20,7 @@ import { Observable, ReplaySubject, } from 'rxjs'; -import { catchError, finalize, map, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { catchError, finalize, map, pluck, shareReplay, switchMap, tap } from 'rxjs/operators'; import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; @@ -45,6 +45,21 @@ import { ExpressionExecutionParams } from '../service'; import { TablesAdapter } from '../util/tables_adapter'; import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter'; +/** + * The result returned after an expression function execution. + */ +export interface ExecutionResult { + /** + * Partial result flag. + */ + partial: boolean; + + /** + * The expression function result. + */ + result: Output; +} + /** * AbortController is not available in Node until v15, so we * need to temporarily mock it for plugins using expressions @@ -91,7 +106,7 @@ export class Execution< /** * Dynamic state of the execution. */ - public readonly state: ExecutionContainer; + public readonly state: ExecutionContainer>; /** * Initial input of the execution. @@ -137,7 +152,7 @@ export class Execution< /** * Future that tracks result or error of this execution. */ - public readonly result: Observable; + public readonly result: Observable>; /** * Keeping track of any child executions @@ -174,7 +189,7 @@ export class Execution< this.expression = execution.expression || formatExpression(execution.ast!); const ast = execution.ast || parseExpression(this.expression); - this.state = createExecutionContainer({ + this.state = createExecutionContainer({ ...executor.state.get(), state: 'not-started', ast, @@ -201,12 +216,40 @@ export class Execution< }; this.result = this.input$.pipe( - switchMap((input) => this.race(this.invokeChain(this.state.get().ast.chain, input))), + switchMap((input) => + this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe( + (source) => + new Observable>((subscriber) => { + let latest: ExecutionResult | undefined; + + subscriber.add( + source.subscribe({ + next: (result) => { + latest = { result, partial: true }; + subscriber.next(latest); + }, + error: (error) => subscriber.error(error), + complete: () => { + if (latest) { + latest.partial = false; + } + + subscriber.complete(); + }, + }) + ); + + subscriber.add(() => { + latest = undefined; + }); + }) + ) + ), catchError((error) => { if (this.abortController.signal.aborted) { this.childExecutions.forEach((childExecution) => childExecution.cancel()); - return of(createAbortErrorValue()); + return of({ result: createAbortErrorValue(), partial: false }); } return throwError(error); @@ -236,25 +279,20 @@ export class Execution< * N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons, * because in legacy interpreter it was set to `null` by default. */ - public start(input: Input = null as any): Observable { + public start( + input: Input = null as any + ): Observable> { if (this.hasStarted) throw new Error('Execution already started.'); this.hasStarted = true; this.input = input; this.state.transitions.start(); if (isObservable(input)) { - // `input$` should never complete - input.subscribe( - (value) => this.input$.next(value), - (error) => this.input$.error(error) - ); + input.subscribe(this.input$); } else if (isPromise(input)) { - input.then( - (value) => this.input$.next(value), - (error) => this.input$.error(error) - ); + from(input).subscribe(this.input$); } else { - this.input$.next(input); + of(input).subscribe(this.input$); } return this.result; @@ -439,6 +477,7 @@ export class Execution< const resolveArgFns = mapValues(dealiasedArgAsts, (asts, argName) => asts.map((item) => (subInput = input) => this.interpret(item, subInput).pipe( + pluck('result'), map((output) => { if (isExpressionValueError(output)) { throw output.error; @@ -486,7 +525,7 @@ export class Execution< }); } - public interpret(ast: ExpressionAstNode, input: T): Observable { + public interpret(ast: ExpressionAstNode, input: T): Observable> { switch (getType(ast)) { case 'expression': const execution = this.execution.executor.createExecution( @@ -494,12 +533,13 @@ export class Execution< this.execution.params ); this.childExecutions.push(execution); + return execution.start(input); case 'string': case 'number': case 'null': case 'boolean': - return of(ast); + return of({ result: ast, partial: false }); default: return throwError(new Error(`Unknown AST object: ${JSON.stringify(ast)}`)); } diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index 99a5c80de3c462..de209f1dfb4a19 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -66,26 +66,23 @@ describe('ExecutionContract', () => { }); }); - test('can get error result of the expression execution', async () => { + test('can get error result of the expression execution', () => { const execution = createExecution('foo bar=123'); const contract = new ExecutionContract(execution); execution.start(); - const result = await contract.getData(); - - expect(result).toMatchObject({ - type: 'error', - }); + expect(contract.getData().toPromise()).resolves.toHaveProperty( + 'result', + expect.objectContaining({ type: 'error' }) + ); }); - test('can get result of the expression execution', async () => { + test('can get result of the expression execution', () => { const execution = createExecution('var_set name="foo" value="bar" | var name="foo"'); const contract = new ExecutionContract(execution); execution.start(); - const result = await contract.getData(); - - expect(result).toBe('bar'); + expect(contract.getData().toPromise()).resolves.toHaveProperty('result', 'bar'); }); describe('isPending', () => { diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts index 3cad9cef5e09ae..445ceb30b58db6 100644 --- a/src/plugins/expressions/common/execution/execution_contract.ts +++ b/src/plugins/expressions/common/execution/execution_contract.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; -import { catchError, take } from 'rxjs/operators'; -import { Execution } from './execution'; +import { of, Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { Execution, ExecutionResult } from './execution'; import { ExpressionValueError } from '../expression_types/specs'; import { ExpressionAstExpression } from '../ast'; @@ -39,22 +39,22 @@ export class ExecutionContract => { - return this.execution.result - .pipe( - take(1), - catchError(({ name, message, stack }) => - of({ + getData = (): Observable> => { + return this.execution.result.pipe( + catchError(({ name, message, stack }) => + of({ + partial: false, + result: { type: 'error', error: { name, message, stack, }, - } as ExpressionValueError) - ) + } as ExpressionValueError, + }) ) - .toPromise(); + ); }; /** diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index a307172aff9737..7ca5a005991bd4 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -13,7 +13,7 @@ import { Observable } from 'rxjs'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; -import { Execution, ExecutionParams } from '../execution/execution'; +import { Execution, ExecutionParams, ExecutionResult } from '../execution/execution'; import { IRegistry } from '../types'; import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; @@ -160,7 +160,7 @@ export class Executor = Record { + ): Observable> { return this.createExecution(ast, params).start(input); } 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 cdcae61215fa42..1b060cde7482d6 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 @@ -65,7 +65,7 @@ describe('expression_functions', () => { it('sets the variables', async () => { const vars = {}; - const result = await executor + const { result } = await executor .run('var_set name=test1 name=test2 value=1', 2, { variables: vars }) .pipe(first()) .toPromise(); diff --git a/src/plugins/expressions/common/service/expressions_services.test.ts b/src/plugins/expressions/common/service/expressions_services.test.ts index 8f86e81f9d1d53..db73d300e1273d 100644 --- a/src/plugins/expressions/common/service/expressions_services.test.ts +++ b/src/plugins/expressions/common/service/expressions_services.test.ts @@ -114,7 +114,7 @@ describe('ExpressionsService', () => { }, }); - const result = await fork.run('__test__', null); + const { result } = await fork.run('__test__', null).toPromise(); expect(result).toBe('123'); }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index b3c01672626614..9e569c66bbe16f 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; -import { ExecutionContract } from '../execution/execution_contract'; -import { AnyExpressionTypeDefinition } from '../expression_types'; +import { ExecutionContract, ExecutionResult } from '../execution'; +import { AnyExpressionTypeDefinition, ExpressionValueError } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; @@ -136,7 +136,7 @@ export interface ExpressionsServiceStart { ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams - ) => Promise; + ) => Observable>; /** * Starts expression execution and immediately returns `ExecutionContract` @@ -243,7 +243,7 @@ export class ExpressionsService implements PersistableStateService this.renderers.register(definition); public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => - this.executor.run(ast, input, params).pipe(take(1)).toPromise(); + this.executor.run(ast, input, params); public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 98adec285afd5e..86477e53dc1a13 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; import { first, skip, toArray } from 'rxjs/operators'; import { loader, ExpressionLoader } from './loader'; import { Observable } from 'rxjs'; @@ -111,8 +112,29 @@ describe('ExpressionLoader', () => { it('emits on $data when data is available', async () => { const expressionLoader = new ExpressionLoader(element, 'var foo', { variables: { foo: 123 } }); - const response = await expressionLoader.data$.pipe(first()).toPromise(); - expect(response).toBe(123); + const { result } = await expressionLoader.data$.pipe(first()).toPromise(); + expect(result).toBe(123); + }); + + it('ignores partial results by default', async () => { + const expressionLoader = new ExpressionLoader(element, 'var foo', { + variables: { foo: of(1, 2) }, + }); + const { result, partial } = await expressionLoader.data$.pipe(first()).toPromise(); + + expect(partial).toBe(false); + expect(result).toBe(2); + }); + + it('emits partial results if enabled', async () => { + const expressionLoader = new ExpressionLoader(element, 'var foo', { + variables: { foo: of(1, 2) }, + partial: true, + }); + const { result, partial } = await expressionLoader.data$.pipe(first()).toPromise(); + + expect(partial).toBe(true); + expect(result).toBe(1); }); it('emits on loading$ on initial load and on updates', async () => { diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 4165b8906a20ee..a51ce35c681804 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; +import { filter, map, delay } from 'rxjs/operators'; import { defaults } from 'lodash'; +import { UnwrapObservable } from '@kbn/utility-types'; import { Adapters } from '../../inspector/public'; import { IExpressionLoaderParams } from './types'; import { ExpressionAstExpression } from '../common'; @@ -20,7 +21,7 @@ import { getExpressionsService } from './services'; type Data = any; export class ExpressionLoader { - data$: Observable; + data$: ReturnType; update$: ExpressionRenderHandler['update$']; render$: ExpressionRenderHandler['render$']; events$: ExpressionRenderHandler['events$']; @@ -28,10 +29,11 @@ export class ExpressionLoader { private execution: ExecutionContract | undefined; private renderHandler: ExpressionRenderHandler; - private dataSubject: Subject; + private dataSubject: Subject>; private loadingSubject: Subject; private data: Data; private params: IExpressionLoaderParams = {}; + private subscription?: Subscription; constructor( element: HTMLElement, @@ -67,8 +69,8 @@ export class ExpressionLoader { } }); - this.data$.subscribe((data) => { - this.render(data); + this.data$.subscribe(({ result }) => { + this.render(result); }); this.render$.subscribe(() => { @@ -87,27 +89,20 @@ export class ExpressionLoader { this.dataSubject.complete(); this.loadingSubject.complete(); this.renderHandler.destroy(); - if (this.execution) { - this.execution.cancel(); - } + this.cancel(); + this.subscription?.unsubscribe(); } cancel() { - if (this.execution) { - this.execution.cancel(); - } + this.execution?.cancel(); } getExpression(): string | undefined { - if (this.execution) { - return this.execution.getExpression(); - } + return this.execution?.getExpression(); } getAst(): ExpressionAstExpression | undefined { - if (this.execution) { - return this.execution.getAst(); - } + return this.execution?.getAst(); } getElement(): HTMLElement { @@ -115,27 +110,25 @@ export class ExpressionLoader { } inspect(): Adapters | undefined { - return this.execution ? (this.execution.inspect() as Adapters) : undefined; + return this.execution?.inspect() as Adapters; } - async update( - expression?: string | ExpressionAstExpression, - params?: IExpressionLoaderParams - ): Promise { + update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void { this.setParams(params); this.loadingSubject.next(true); if (expression) { - await this.loadData(expression, this.params); + this.loadData(expression, this.params); } else if (this.data) { this.render(this.data); } } - private loadData = async ( + private loadData = ( expression: string | ExpressionAstExpression, params: IExpressionLoaderParams - ): Promise => { + ) => { + this.subscription?.unsubscribe(); if (this.execution && this.execution.isPending) { this.execution.cancel(); } @@ -148,13 +141,13 @@ export class ExpressionLoader { debug: params.debug, syncColors: params.syncColors, }); - - const prevDataHandler = this.execution; - const data = await prevDataHandler.getData(); - if (this.execution !== prevDataHandler) { - return; - } - this.dataSubject.next(data); + this.subscription = this.execution + .getData() + .pipe( + delay(0), // delaying until the next tick since we execute the expression in the constructor + filter(({ partial }) => params.partial || !partial) + ) + .subscribe((value) => this.dataSubject.next(value)); }; private render(data: Data): void { @@ -184,6 +177,7 @@ export class ExpressionLoader { } this.params.syncColors = params.syncColors; this.params.debug = Boolean(params.debug); + this.params.partial = Boolean(params.partial); this.params.inspectorAdapters = (params.inspectorAdapters || this.execution?.inspect()) as Adapters; diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index 93353ebbb51b61..1963eb1f1b3f7b 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -36,8 +36,10 @@ describe('ExpressionsPublicPlugin', () => { describe('.run()', () => { test('can execute simple expression', async () => { const { setup } = await expressionsPluginMock.createPlugin(); - const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null); - expect(bar).toBe('bar'); + const { result } = await setup + .run('var_set name="foo" value="bar" | var name="foo"', null) + .toPromise(); + expect(result).toBe('bar'); }); }); }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 97009ae543b974..2d9c6d94cfa6dc 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -112,16 +112,17 @@ export class Execution(ast: ExpressionAstNode, input: T): Observable; + interpret(ast: ExpressionAstNode, input: T): Observable>; // (undocumented) invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; // (undocumented) invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; // (undocumented) resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; - readonly result: Observable; - start(input?: Input): Observable; - readonly state: ExecutionContainer; + readonly result: Observable>; + start(input?: Input): Observable>; + // Warning: (ae-forgotten-export) The symbol "ExecutionResult" needs to be exported by the entry point index.d.ts + readonly state: ExecutionContainer>; } // Warning: (ae-forgotten-export) The symbol "StateContainer" needs to be exported by the entry point index.d.ts @@ -155,7 +156,7 @@ export class ExecutionContract; getAst: () => ExpressionAstExpression; - getData: () => Promise; + getData: () => Observable>; getExpression: () => string; inspect: () => InspectorAdapters; // (undocumented) @@ -230,7 +231,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable>; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -637,7 +638,7 @@ export interface ExpressionsServiceStart { getFunction: (name: string) => ReturnType; getRenderer: (name: string) => ReturnType; getType: (name: string) => ReturnType; - run: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise; + run: (ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Observable>; } // Warning: (ae-missing-release-tag) "ExpressionsSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -907,6 +908,8 @@ export interface IExpressionLoaderParams { // // (undocumented) onRenderError?: RenderErrorHandlerFnType; + // (undocumented) + partial?: boolean; // Warning: (ae-forgotten-export) The symbol "RenderMode" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1073,7 +1076,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { // (undocumented) expression: string | ExpressionAstExpression; // (undocumented) - onData$?: (data: TData, adapters?: TInspectorAdapters) => void; + onData$?: (data: TData, adapters?: TInspectorAdapters, partial?: boolean) => void; // (undocumented) onEvent?: (event: ExpressionRendererEvent) => void; // (undocumented) diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index e7ae6ee45ab600..d31a4c947b09d4 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -255,7 +255,7 @@ describe('ExpressionRenderer', () => { const dataSubject = new Subject(); const data$ = dataSubject.asObservable().pipe(share()); - const newData = {}; + const result = {}; const inspectData = {}; const onData$ = jest.fn(); @@ -275,11 +275,11 @@ describe('ExpressionRenderer', () => { expect(onData$).toHaveBeenCalledTimes(0); act(() => { - dataSubject.next(newData); + dataSubject.next({ result }); }); expect(onData$).toHaveBeenCalledTimes(1); - expect(onData$.mock.calls[0][0]).toBe(newData); + expect(onData$.mock.calls[0][0]).toBe(result); expect(onData$.mock.calls[0][1]).toBe(inspectData); }); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index ce8547f132c279..719af1b39f89e0 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -30,7 +30,11 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { ) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; onEvent?: (event: ExpressionRendererEvent) => void; - onData$?: (data: TData, adapters?: TInspectorAdapters) => void; + onData$?: ( + data: TData, + adapters?: TInspectorAdapters, + partial?: boolean + ) => void; /** * An observable which can be used to re-run the expression without destroying the component */ @@ -135,8 +139,8 @@ export const ReactExpressionRenderer = ({ } if (onData$) { subs.push( - expressionLoaderRef.current.data$.subscribe((newData) => { - onData$(newData, expressionLoaderRef.current?.inspect()); + expressionLoaderRef.current.data$.subscribe(({ partial, result }) => { + onData$(result, expressionLoaderRef.current?.inspect(), partial); }) ); } diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 947b84ec152acb..2375252e827848 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -48,6 +48,7 @@ export interface IExpressionLoaderParams { renderMode?: RenderMode; syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; + partial?: boolean; } export interface ExpressionRenderError extends Error { diff --git a/src/plugins/expressions/server/plugin.test.ts b/src/plugins/expressions/server/plugin.test.ts index d967f9e614678e..c41cda36e7623a 100644 --- a/src/plugins/expressions/server/plugin.test.ts +++ b/src/plugins/expressions/server/plugin.test.ts @@ -28,8 +28,10 @@ describe('ExpressionsServerPlugin', () => { describe('.run()', () => { test('can execute simple expression', async () => { const { setup } = await expressionsPluginMock.createPlugin(); - const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null); - expect(bar).toBe('bar'); + const { result } = await setup + .run('var_set name="foo" value="bar" | var name="foo"', null) + .toPromise(); + expect(result).toBe('bar'); }); }); }); diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 8d2e113e6b6ed4..ec16d95ea8a3ff 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -110,16 +110,17 @@ export class Execution(ast: ExpressionAstNode, input: T): Observable; + interpret(ast: ExpressionAstNode, input: T): Observable>; // (undocumented) invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable; // (undocumented) invokeFunction(fn: ExpressionFunction, input: unknown, args: Record): Observable; // (undocumented) resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable; - readonly result: Observable; - start(input?: Input): Observable; - readonly state: ExecutionContainer; + readonly result: Observable>; + start(input?: Input): Observable>; + // Warning: (ae-forgotten-export) The symbol "ExecutionResult" needs to be exported by the entry point index.d.ts + readonly state: ExecutionContainer>; } // Warning: (ae-forgotten-export) The symbol "StateContainer" needs to be exported by the entry point index.d.ts @@ -212,7 +213,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable; + run(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable>; // (undocumented) readonly state: ExecutorContainer; // (undocumented) diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 65857f02c883d9..54a3fe9e4399c0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -129,6 +129,7 @@ export const applicationUsageSchema = { error: commonSchema, status: commonSchema, kibanaOverview: commonSchema, + r: commonSchema, // X-Pack apm: commonSchema, diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts index 809cb15c3e9606..18f59186f61831 100644 --- a/src/plugins/kibana_utils/common/persistable_state/index.ts +++ b/src/plugins/kibana_utils/common/persistable_state/index.ts @@ -6,87 +6,5 @@ * Side Public License, v 1. */ -import { SavedObjectReference } from '../../../../core/types'; - -export type SerializableValue = string | number | boolean | null | undefined | SerializableState; -export type Serializable = SerializableValue | SerializableValue[]; - -export type SerializableState = { - [key: string]: Serializable; -}; - -export type MigrateFunction< - FromVersion extends SerializableState = SerializableState, - ToVersion extends SerializableState = SerializableState -> = (state: FromVersion) => ToVersion; - -export type MigrateFunctionsObject = { - [key: string]: MigrateFunction; -}; - -export interface PersistableStateService

{ - /** - * function to extract telemetry information - * @param state - * @param collector - */ - telemetry: (state: P, collector: Record) => Record; - /** - * inject function receives state and a list of references and should return state with references injected - * default is identity function - * @param state - * @param references - */ - inject: (state: P, references: SavedObjectReference[]) => P; - /** - * extract function receives state and should return state with references extracted and array of references - * default returns same state with empty reference array - * @param state - */ - extract: (state: P) => { state: P; references: SavedObjectReference[] }; - - /** - * migrateToLatest function receives state of older version and should migrate to the latest version - * @param state - * @param version - */ - migrateToLatest?: (state: SerializableState, version: string) => P; - - /** - * migrate function runs the specified migration - * @param state - * @param version - */ - migrate: (state: SerializableState, version: string) => SerializableState; -} - -export interface PersistableState

{ - /** - * function to extract telemetry information - * @param state - * @param collector - */ - telemetry: (state: P, collector: Record) => Record; - /** - * inject function receives state and a list of references and should return state with references injected - * default is identity function - * @param state - * @param references - */ - inject: (state: P, references: SavedObjectReference[]) => P; - /** - * extract function receives state and should return state with references extracted and array of references - * default returns same state with empty reference array - * @param state - */ - extract: (state: P) => { state: P; references: SavedObjectReference[] }; - - /** - * list of all migrations per semver - */ - migrations: MigrateFunctionsObject; -} - -export type PersistableStateDefinition

= Partial< - PersistableState

->; +export * from './types'; +export { migrateToLatest } from './migrate_to_latest'; diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts new file mode 100644 index 00000000000000..2ae376e787d2f7 --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SerializableState, MigrateFunction } from './types'; +import { migrateToLatest } from './migrate_to_latest'; + +interface StateV1 extends SerializableState { + name: string; +} + +interface StateV2 extends SerializableState { + firstName: string; + lastName: string; +} + +interface StateV3 extends SerializableState { + firstName: string; + lastName: string; + isAdmin: boolean; + age: number; +} + +const migrationV2: MigrateFunction = ({ name }) => { + return { + firstName: name, + lastName: '', + }; +}; + +const migrationV3: MigrateFunction = ({ firstName, lastName }) => { + return { + firstName, + lastName, + isAdmin: false, + age: 0, + }; +}; + +test('returns the same object if there are no migrations to be applied', () => { + const migrated = migrateToLatest( + {}, + { + state: { name: 'Foo' }, + version: '0.0.1', + } + ); + + expect(migrated).toEqual({ + state: { name: 'Foo' }, + version: '0.0.1', + }); +}); + +test('applies a single migration', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.0.2': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.0.1', + } + ); + + expect(newState).toEqual({ + firstName: 'Foo', + lastName: '', + }); + expect(newVersion).toEqual('0.0.2'); +}); + +test('does not apply migration if it has the same version as state', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.0.54': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.0.54', + } + ); + + expect(newState).toEqual({ + name: 'Foo', + }); + expect(newVersion).toEqual('0.0.54'); +}); + +test('does not apply migration if it has lower version', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.2.2': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.3.1', + } + ); + + expect(newState).toEqual({ + name: 'Foo', + }); + expect(newVersion).toEqual('0.3.1'); +}); + +test('applies two migrations consecutively', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '7.14.0': (migrationV2 as unknown) as MigrateFunction, + '7.14.2': (migrationV3 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '7.13.4', + } + ); + + expect(newState).toEqual({ + firstName: 'Foo', + lastName: '', + isAdmin: false, + age: 0, + }); + expect(newVersion).toEqual('7.14.2'); +}); + +test('applies only migrations which are have higher semver version', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '7.14.0': (migrationV2 as unknown) as MigrateFunction, // not applied + '7.14.1': (() => ({})) as MigrateFunction, // not applied + '7.14.2': (migrationV3 as unknown) as MigrateFunction, + }, + { + state: { firstName: 'FooBar', lastName: 'Baz' }, + version: '7.14.1', + } + ); + + expect(newState).toEqual({ + firstName: 'FooBar', + lastName: 'Baz', + isAdmin: false, + age: 0, + }); + expect(newVersion).toEqual('7.14.2'); +}); diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts new file mode 100644 index 00000000000000..c16392164e3e4a --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { compare } from 'semver'; +import { SerializableState, VersionedState, MigrateFunctionsObject } from './types'; + +export function migrateToLatest( + migrations: MigrateFunctionsObject, + { state, version: oldVersion }: VersionedState +): VersionedState { + const versions = Object.keys(migrations || {}) + .filter((v) => compare(v, oldVersion) > 0) + .sort(compare); + + if (!versions.length) return { state, version: oldVersion } as VersionedState; + + for (const version of versions) { + state = migrations[version]!(state); + } + + return { + state: state as S, + version: versions[versions.length - 1], + }; +} diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts new file mode 100644 index 00000000000000..f7168b46e7fca6 --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectReference } from '../../../../core/types'; + +/** + * Serializable state is something is a POJO JavaScript object that can be + * serialized to a JSON string. + */ +export type SerializableState = { + [key: string]: Serializable; +}; +export type SerializableValue = string | number | boolean | null | undefined | SerializableState; +export type Serializable = SerializableValue | SerializableValue[]; + +/** + * Versioned state is a POJO JavaScript object that can be serialized to JSON, + * and which also contains the version information. The version is stored in + * semver format and corresponds to the Kibana release version when the object + * was created. The version can be used to apply migrations to the object. + * + * For example: + * + * ```ts + * const obj: VersionedState<{ dashboardId: string }> = { + * version: '7.14.0', + * state: { + * dashboardId: '123', + * }, + * }; + * ``` + */ +export interface VersionedState { + version: string; + state: S; +} + +/** + * Persistable state interface can be implemented by something that persists + * (stores) state, for example, in a saved object. Once implemented that thing + * will gain ability to "extract" and "inject" saved object references, which + * are necessary for various saved object tasks, such as export. It will also be + * able to do state migrations across Kibana versions, if the shape of the state + * would change over time. + * + * @todo Maybe rename it to `PersistableStateItem`? + */ +export interface PersistableState

{ + /** + * Function which reports telemetry information. This function is essentially + * a "reducer" - it receives the existing "stats" object and returns an + * updated version of the "stats" object. + * + * @param state The persistable state serializable state object. + * @param stats Stats object containing the stats which were already + * collected. This `stats` object shall not be mutated in-line. + * @returns A new stats object augmented with new telemetry information. + */ + telemetry: (state: P, stats: Record) => Record; + + /** + * A function which receives state and a list of references and should return + * back the state with references injected. The default is an identity + * function. + * + * @param state The persistable state serializable state object. + * @param references List of saved object references. + * @returns Persistable state object with references injected. + */ + inject: (state: P, references: SavedObjectReference[]) => P; + + /** + * A function which receives state and should return the state with references + * extracted and an array of the extracted references. The default case could + * simply return the same state with an empty array of references. + * + * @param state The persistable state serializable state object. + * @returns Persistable state object with references extracted and a list of + * references. + */ + extract: (state: P) => { state: P; references: SavedObjectReference[] }; + + /** + * A list of migration functions, which migrate the persistable state + * serializable object to the next version. Migration functions should are + * keyed by the Kibana version using semver, where the version indicates to + * which version the state will be migrated to. + */ + migrations: MigrateFunctionsObject; +} + +/** + * Collection of migrations that a given type of persistable state object has + * accumulated over time. Migration functions are keyed using semver version + * of Kibana releases. + */ +export type MigrateFunctionsObject = { [semver: string]: MigrateFunction }; +export type MigrateFunction< + FromVersion extends SerializableState = SerializableState, + ToVersion extends SerializableState = SerializableState +> = (state: FromVersion) => ToVersion; + +/** + * @todo Shall we remove this? + */ +export type PersistableStateDefinition

= Partial< + PersistableState

+>; + +/** + * @todo Add description. + */ +export interface PersistableStateService

{ + /** + * Function which reports telemetry information. This function is essentially + * a "reducer" - it receives the existing "stats" object and returns an + * updated version of the "stats" object. + * + * @param state The persistable state serializable state object. + * @param stats Stats object containing the stats which were already + * collected. This `stats` object shall not be mutated in-line. + * @returns A new stats object augmented with new telemetry information. + */ + telemetry(state: P, collector: Record): Record; + + /** + * A function which receives state and a list of references and should return + * back the state with references injected. The default is an identity + * function. + * + * @param state The persistable state serializable state object. + * @param references List of saved object references. + * @returns Persistable state object with references injected. + */ + inject(state: P, references: SavedObjectReference[]): P; + + /** + * A function which receives state and should return the state with references + * extracted and an array of the extracted references. The default case could + * simply return the same state with an empty array of references. + * + * @param state The persistable state serializable state object. + * @returns Persistable state object with references extracted and a list of + * references. + */ + extract(state: P): { state: P; references: SavedObjectReference[] }; + + /** + * Migrate function runs a specified migration of a {@link PersistableState} + * item. + * + * When using this method it is up to consumer to make sure that the + * migration function are executed in the right semver order. To avoid such + * potentially error prone complexity, prefer using `migrateToLatest` method + * instead. + * + * @param state The old persistable state serializable state object, which + * needs a migration. + * @param version Semver version of the migration to execute. + * @returns Persistable state object updated with the specified migration + * applied to it. + */ + migrate(state: SerializableState, version: string): SerializableState; + + /** + * A function which receives the state of an older object and version and + * should migrate the state of the object to the latest possible version using + * the `.migrations` dictionary provided on a {@link PersistableState} item. + * + * @param state The persistable state serializable state object. + * @param version Current semver version of the `state`. + * @returns A serializable state object migrated to the latest state. + */ + migrateToLatest?: (state: VersionedState) => VersionedState

; +} diff --git a/src/plugins/share/common/mocks.ts b/src/plugins/share/common/mocks.ts new file mode 100644 index 00000000000000..6768c1aff810a3 --- /dev/null +++ b/src/plugins/share/common/mocks.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './url_service/mocks'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 680fb2231fc48d..bae57b6d8a31d2 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -30,7 +30,7 @@ export interface LocatorDependencies { getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } -export class Locator

implements PersistableState

, LocatorPublic

{ +export class Locator

implements LocatorPublic

{ public readonly migrations: PersistableState

['migrations']; constructor( diff --git a/src/plugins/share/common/url_service/mocks.ts b/src/plugins/share/common/url_service/mocks.ts new file mode 100644 index 00000000000000..be86cfe4017133 --- /dev/null +++ b/src/plugins/share/common/url_service/mocks.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable max-classes-per-file */ + +import type { LocatorDefinition, KibanaLocation } from '.'; +import { UrlService } from '.'; + +export class MockUrlService extends UrlService { + constructor() { + super({ + navigate: async () => {}, + getUrl: async ({ app, path }, { absolute }) => { + return `${absolute ? 'https://example.com' : ''}/app/${app}${path}`; + }, + }); + } +} + +export class MockLocatorDefinition implements LocatorDefinition { + constructor(public readonly id: string) {} + + public readonly getLocation = async (): Promise => { + return { + app: 'test', + path: '/test', + state: { + foo: 'bar', + }, + }; + }; +} diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 5ee3156534c5ef..1f999b59ddb617 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -9,6 +9,7 @@ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service'; +export { parseSearchParams, formatSearchParams } from './url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts new file mode 100644 index 00000000000000..eb9c6d0d109063 --- /dev/null +++ b/src/plugins/share/public/mocks.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '../common/mocks'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 893108b56bcfad..adc28556d7a3cc 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -19,6 +19,7 @@ import { UrlGeneratorsStart, } from './url_generators/url_generator_service'; import { UrlService } from '../common/url_service'; +import { RedirectManager } from './url_service'; export interface ShareSetupDependencies { securityOss?: SecurityOssPluginSetup; @@ -86,6 +87,11 @@ export class SharePlugin implements Plugin { }, }); + const redirectManager = new RedirectManager({ + url: this.url, + }); + redirectManager.registerRedirectApp(core); + return { ...this.shareMenuRegistry.setup(), urlGenerators: this.urlGeneratorsService.setup(core), diff --git a/src/plugins/share/public/url_service/index.ts b/src/plugins/share/public/url_service/index.ts new file mode 100644 index 00000000000000..8fa88e9c570bd4 --- /dev/null +++ b/src/plugins/share/public/url_service/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './redirect'; diff --git a/src/plugins/share/public/url_service/redirect/README.md b/src/plugins/share/public/url_service/redirect/README.md new file mode 100644 index 00000000000000..cd31f2b80099be --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/README.md @@ -0,0 +1,18 @@ +# Redirect endpoint + +This folder contains implementation of *the Redirect Endpoint*. The Redirect +Endpoint receives parameters of a locator and then "redirects" the user using +navigation without page refresh to the location targeted by the locator. While +using the locator, it is also possible to set the *location state* of the +target page. Location state is a serializable object which can be passed to +the destination app while navigating without a page reload. + +``` +/app/r?l=MY_LOCATOR&v=7.14.0&p=(dashboardId:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +``` + +For example: + +``` +/app/r?l=DISCOVER_APP_LOCATOR&v=7.14.0&p={%22indexPatternId%22:%22d3d7af60-4c81-11e8-b3d7-01146121b73d%22} +``` diff --git a/src/plugins/share/public/url_service/redirect/components/error.tsx b/src/plugins/share/public/url_service/redirect/components/error.tsx new file mode 100644 index 00000000000000..716848427c638a --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/error.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as React from 'react'; +import { + EuiEmptyPrompt, + EuiCallOut, + EuiCodeBlock, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const defaultTitle = i18n.translate('share.urlService.redirect.components.Error.title', { + defaultMessage: 'Redirection error', + description: + 'Title displayed to user in redirect endpoint when redirection cannot be performed successfully.', +}); + +export interface ErrorProps { + title?: string; + error: Error; +} + +export const Error: React.FC = ({ title = defaultTitle, error }) => { + return ( + {title}

} + body={ + + + + {error.message} + + + + + {error.stack ? error.stack : ''} + + + } + /> + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/components/page.tsx b/src/plugins/share/public/url_service/redirect/components/page.tsx new file mode 100644 index 00000000000000..805213b73fdd06 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/page.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { EuiPageTemplate } from '@elastic/eui'; +import { Error } from './error'; +import { RedirectManager } from '../redirect_manager'; +import { Spinner } from './spinner'; + +export interface PageProps { + manager: Pick; +} + +export const Page: React.FC = ({ manager }) => { + const error = useObservable(manager.error$); + + if (error) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/components/spinner.tsx b/src/plugins/share/public/url_service/redirect/components/spinner.tsx new file mode 100644 index 00000000000000..a70ae5eb096aff --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/spinner.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const text = i18n.translate('share.urlService.redirect.components.Spinner.label', { + defaultMessage: 'Redirecting…', + description: 'Redirect endpoint spinner label.', +}); + +export const Spinner: React.FC = () => { + return ( + + + + + + + + + {text} + + + + + + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/index.ts b/src/plugins/share/public/url_service/redirect/index.ts new file mode 100644 index 00000000000000..8dbc5f4e0ab1c3 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './redirect_manager'; +export { formatSearchParams } from './util/format_search_params'; +export { parseSearchParams } from './util/parse_search_params'; diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts new file mode 100644 index 00000000000000..f610268f529bc9 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RedirectManager } from './redirect_manager'; +import { MockUrlService } from '../../mocks'; +import { MigrateFunction } from 'src/plugins/kibana_utils/common'; + +const setup = () => { + const url = new MockUrlService(); + const locator = url.locators.create({ + id: 'TEST_LOCATOR', + getLocation: async () => { + return { + app: '', + path: '', + state: {}, + }; + }, + migrations: { + '0.0.2': ((({ num }: { num: number }) => ({ num: num * 2 })) as unknown) as MigrateFunction, + }, + }); + const manager = new RedirectManager({ + url, + }); + + return { + url, + locator, + manager, + }; +}; + +describe('on page mount', () => { + test('execute locator "navigate" method', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + expect(spy).toHaveBeenCalledTimes(0); + manager.onMount(`l=TEST_LOCATOR&v=0.0.3&p=${encodeURIComponent(JSON.stringify({}))}`); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('passes arguments provided in URL to locator "navigate" method', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + manager.onMount( + `l=TEST_LOCATOR&v=0.0.3&p=${encodeURIComponent( + JSON.stringify({ + foo: 'bar', + }) + )}` + ); + expect(spy).toHaveBeenCalledWith({ + foo: 'bar', + }); + }); + + test('migrates parameters on-the-fly to the latest version', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + manager.onMount( + `l=TEST_LOCATOR&v=0.0.1&p=${encodeURIComponent( + JSON.stringify({ + num: 1, + }) + )}` + ); + expect(spy).toHaveBeenCalledWith({ + num: 2, + }); + }); + + test('throws if locator does not exist', async () => { + const { manager } = setup(); + + expect(() => + manager.onMount( + `l=TEST_LOCATOR_WHICH_DOES_NOT_EXIST&v=0.0.3&p=${encodeURIComponent(JSON.stringify({}))}` + ) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator [ID = TEST_LOCATOR_WHICH_DOES_NOT_EXIST] does not exist."` + ); + }); +}); diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts new file mode 100644 index 00000000000000..6148249f5a047a --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { CoreSetup } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { migrateToLatest } from '../../../../kibana_utils/common'; +import type { SerializableState } from '../../../../kibana_utils/common'; +import type { UrlService } from '../../../common/url_service'; +import { render } from './render'; +import { parseSearchParams } from './util/parse_search_params'; + +export interface RedirectOptions { + /** Locator ID. */ + id: string; + + /** Kibana version when locator params where generated. */ + version: string; + + /** Locator params. */ + params: unknown & SerializableState; +} + +export interface RedirectManagerDependencies { + url: UrlService; +} + +export class RedirectManager { + public readonly error$ = new BehaviorSubject(null); + + constructor(public readonly deps: RedirectManagerDependencies) {} + + public registerRedirectApp(core: CoreSetup) { + core.application.register({ + id: 'r', + title: 'Redirect endpoint', + chromeless: true, + mount: (params) => { + const unmount = render(params.element, { manager: this }); + this.onMount(params.history.location.search); + return () => { + unmount(); + }; + }, + }); + } + + public onMount(urlLocationSearch: string) { + const options = this.parseSearchParams(urlLocationSearch); + const locator = this.deps.url.locators.get(options.id); + + if (!locator) { + const message = i18n.translate('share.urlService.redirect.RedirectManager.locatorNotFound', { + defaultMessage: 'Locator [ID = {id}] does not exist.', + values: { + id: options.id, + }, + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because locator does not exist.', + }); + const error = new Error(message); + this.error$.next(error); + throw error; + } + + const { state: migratedParams } = migrateToLatest(locator.migrations, { + state: options.params, + version: options.version, + }); + + locator + .navigate(migratedParams) + .then() + .catch((error) => { + // eslint-disable-next-line no-console + console.log('Redirect endpoint failed to execute locator redirect.'); + // eslint-disable-next-line no-console + console.error(error); + }); + } + + protected parseSearchParams(urlLocationSearch: string): RedirectOptions { + try { + return parseSearchParams(urlLocationSearch); + } catch (error) { + this.error$.next(error); + throw error; + } + } +} diff --git a/src/plugins/share/public/url_service/redirect/render.ts b/src/plugins/share/public/url_service/redirect/render.ts new file mode 100644 index 00000000000000..2b9c3a50758e4a --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/render.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Page, PageProps } from './components/page'; + +export const render = (container: HTMLElement, props: PageProps) => { + ReactDOM.render(React.createElement(Page, props), container); + + return () => { + ReactDOM.unmountComponentAtNode(container); + }; +}; diff --git a/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts b/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts new file mode 100644 index 00000000000000..f8d8d6a6295d96 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { formatSearchParams } from './format_search_params'; +import { parseSearchParams } from './parse_search_params'; + +test('can format typical locator settings as URL path search params', () => { + const search = formatSearchParams({ + id: 'LOCATOR_ID', + version: '7.21.3', + params: { + dashboardId: '123', + mode: 'edit', + }, + }); + + expect(search.get('l')).toBe('LOCATOR_ID'); + expect(search.get('v')).toBe('7.21.3'); + expect(JSON.parse(search.get('p')!)).toEqual({ + dashboardId: '123', + mode: 'edit', + }); +}); + +test('can format and then parse redirect options', () => { + const options = { + id: 'LOCATOR_ID', + version: '7.21.3', + params: { + dashboardId: '123', + mode: 'edit', + }, + }; + const formatted = formatSearchParams(options); + const parsed = parseSearchParams(formatted.toString()); + + expect(parsed).toEqual(options); +}); diff --git a/src/plugins/share/public/url_service/redirect/util/format_search_params.ts b/src/plugins/share/public/url_service/redirect/util/format_search_params.ts new file mode 100644 index 00000000000000..12c6424182a876 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/format_search_params.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RedirectOptions } from '../redirect_manager'; + +export function formatSearchParams(opts: RedirectOptions): URLSearchParams { + const searchParams = new URLSearchParams(); + + searchParams.set('l', opts.id); + searchParams.set('v', opts.version); + searchParams.set('p', JSON.stringify(opts.params)); + + return searchParams; +} diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts new file mode 100644 index 00000000000000..418e21cfd40532 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseSearchParams } from './parse_search_params'; + +test('parses a well constructed URL path search part', () => { + const res = parseSearchParams(`?l=LOCATOR&v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`); + + expect(res).toEqual({ + id: 'LOCATOR', + version: '0.0.0', + params: { + foo: 'bar', + }, + }); +}); + +test('throws on missing locator ID', () => { + expect(() => + parseSearchParams(`?v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator ID not specified. Specify \\"l\\" search parameter in the URL, which should be an existing locator ID."` + ); + + expect(() => + parseSearchParams(`?l=&v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator ID not specified. Specify \\"l\\" search parameter in the URL, which should be an existing locator ID."` + ); +}); + +test('throws on missing version', () => { + expect(() => + parseSearchParams(`?l=LOCATOR&v=&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator params version not specified. Specify \\"v\\" search parameter in the URL, which should be the release version of Kibana when locator params were generated."` + ); + + expect(() => + parseSearchParams(`?l=LOCATOR&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator params version not specified. Specify \\"v\\" search parameter in the URL, which should be the release version of Kibana when locator params were generated."` + ); +}); + +test('throws on missing params', () => { + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1`)).toThrowErrorMatchingInlineSnapshot( + `"Locator params not specified. Specify \\"p\\" search parameter in the URL, which should be JSON serialized object of locator params."` + ); + + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1&p=`)).toThrowErrorMatchingInlineSnapshot( + `"Locator params not specified. Specify \\"p\\" search parameter in the URL, which should be JSON serialized object of locator params."` + ); +}); + +test('throws if params are not JSON', () => { + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1&p=asdf`)).toThrowErrorMatchingInlineSnapshot( + `"Could not parse locator params. Locator params must be serialized as JSON and set at \\"p\\" URL search parameter."` + ); +}); diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts new file mode 100644 index 00000000000000..a60c1d1b68a97a --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import { i18n } from '@kbn/i18n'; +import type { RedirectOptions } from '../redirect_manager'; + +/** + * Parses redirect endpoint URL path search parameters. Expects them in the + * following form: + * + * ``` + * /r?l=&v=&p= + * ``` + * + * @param urlSearch Search part of URL path. + * @returns Parsed out locator ID, version, and locator params. + */ +export function parseSearchParams(urlSearch: string): RedirectOptions { + const search = new URLSearchParams(urlSearch); + const id = search.get('l'); + const version = search.get('v'); + const paramsJson = search.get('p'); + + if (!id) { + const message = i18n.translate( + 'share.urlService.redirect.RedirectManager.missingParamLocator', + { + defaultMessage: + 'Locator ID not specified. Specify "l" search parameter in the URL, which should be an existing locator ID.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing locator ID.', + } + ); + throw new Error(message); + } + + if (!version) { + const message = i18n.translate( + 'share.urlService.redirect.RedirectManager.missingParamVersion', + { + defaultMessage: + 'Locator params version not specified. Specify "v" search parameter in the URL, which should be the release version of Kibana when locator params were generated.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing version parameter.', + } + ); + throw new Error(message); + } + + if (!paramsJson) { + const message = i18n.translate('share.urlService.redirect.RedirectManager.missingParamParams', { + defaultMessage: + 'Locator params not specified. Specify "p" search parameter in the URL, which should be JSON serialized object of locator params.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing params parameter.', + }); + throw new Error(message); + } + + let params: unknown & SerializableState; + try { + params = JSON.parse(paramsJson); + } catch { + const message = i18n.translate('share.urlService.redirect.RedirectManager.invalidParamParams', { + defaultMessage: + 'Could not parse locator params. Locator params must be serialized as JSON and set at "p" URL search parameter.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because locator parameters could not be parsed as JSON.', + }); + throw new Error(message); + } + + return { + id, + version, + params, + }; +} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index d11e1cf78c9606..13caa3c33fa827 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1743,6 +1743,137 @@ } } }, + "r": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "apm": { "properties": { "appId": { diff --git a/src/plugins/user_setup/README.md b/src/plugins/user_setup/README.md new file mode 100644 index 00000000000000..61ec964f5bb80c --- /dev/null +++ b/src/plugins/user_setup/README.md @@ -0,0 +1,3 @@ +# `userSetup` plugin + +The plugin provides UI and APIs for the interactive setup mode. diff --git a/src/plugins/user_setup/jest.config.js b/src/plugins/user_setup/jest.config.js new file mode 100644 index 00000000000000..75e355e230c5db --- /dev/null +++ b/src/plugins/user_setup/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/user_setup'], +}; diff --git a/src/plugins/user_setup/kibana.json b/src/plugins/user_setup/kibana.json new file mode 100644 index 00000000000000..192fd42cd3e264 --- /dev/null +++ b/src/plugins/user_setup/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "userSetup", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin provides UI and APIs for the interactive setup mode.", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["userSetup"], + "server": true, + "ui": true +} diff --git a/src/plugins/user_setup/public/app.tsx b/src/plugins/user_setup/public/app.tsx new file mode 100644 index 00000000000000..2b6b7089539723 --- /dev/null +++ b/src/plugins/user_setup/public/app.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiPageTemplate, EuiPanel, EuiText } from '@elastic/eui'; +import React from 'react'; + +export const App = () => { + return ( + + + Kibana server is not ready yet. + + + ); +}; diff --git a/src/plugins/user_setup/public/index.ts b/src/plugins/user_setup/public/index.ts new file mode 100644 index 00000000000000..153bc92a0dd087 --- /dev/null +++ b/src/plugins/user_setup/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UserSetupPlugin } from './plugin'; + +export const plugin = () => new UserSetupPlugin(); diff --git a/src/plugins/user_setup/public/plugin.tsx b/src/plugins/user_setup/public/plugin.tsx new file mode 100644 index 00000000000000..677c27cc456dca --- /dev/null +++ b/src/plugins/user_setup/public/plugin.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { App } from './app'; + +export class UserSetupPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ + id: 'userSetup', + title: 'User Setup', + chromeless: true, + mount: (params) => { + ReactDOM.render(, params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); + }, + }); + } + + public start(core: CoreStart) {} +} diff --git a/src/plugins/user_setup/server/config.ts b/src/plugins/user_setup/server/config.ts new file mode 100644 index 00000000000000..b16c51bcbda09d --- /dev/null +++ b/src/plugins/user_setup/server/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +export type ConfigType = TypeOf; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); diff --git a/src/plugins/user_setup/server/index.ts b/src/plugins/user_setup/server/index.ts new file mode 100644 index 00000000000000..2a43cbbf65c9de --- /dev/null +++ b/src/plugins/user_setup/server/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'src/core/server'; + +import { ConfigSchema } from './config'; +import { UserSetupPlugin } from './plugin'; + +export const config: PluginConfigDescriptor> = { + schema: ConfigSchema, +}; + +export const plugin = () => new UserSetupPlugin(); diff --git a/src/plugins/user_setup/server/plugin.ts b/src/plugins/user_setup/server/plugin.ts new file mode 100644 index 00000000000000..918c9a20079352 --- /dev/null +++ b/src/plugins/user_setup/server/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { CoreSetup, CoreStart, Plugin } from 'src/core/server'; + +export class UserSetupPlugin implements Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/user_setup/tsconfig.json b/src/plugins/user_setup/tsconfig.json new file mode 100644 index 00000000000000..d211a70f12df33 --- /dev/null +++ b/src/plugins/user_setup/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [{ "path": "../../core/tsconfig.json" }] +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js index bb264aaacbfbfc..801681dbd531f1 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { pluck } from 'rxjs/operators'; const MIN_CHART_HEIGHT = 300; @@ -55,7 +56,9 @@ class VisEditorVisualizationUI extends Component { await this._handler.render(this._visEl.current); this.props.eventEmitter.emit('embeddableRendered'); - this._subscription = this._handler.handler.data$.subscribe((data) => onDataChange(data.value)); + this._subscription = this._handler.handler.data$ + .pipe(pluck('result')) + .subscribe((data) => onDataChange(data.value)); } /** diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js index 7f72d44c50ea00..2efc145b12561b 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.js @@ -21,7 +21,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'discover', 'timePicker']); const kibanaServer = getService('kibanaServer'); - describe('discover - context - back navigation', function contextSize() { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104364 + describe.skip('discover - context - back navigation', function contextSize() { before(async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index a09be8b35ba8f6..6a2298ba48cb45 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -32,7 +32,8 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); - describe('context link in discover', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104413 + describe.skip('context link in discover', () => { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ diff --git a/test/functional/apps/dashboard/saved_search_embeddable.ts b/test/functional/apps/dashboard/saved_search_embeddable.ts index 5bcec338aad1eb..33d015a4c60199 100644 --- a/test/functional/apps/dashboard/saved_search_embeddable.ts +++ b/test/functional/apps/dashboard/saved_search_embeddable.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); - describe('dashboard saved search embeddable', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104365 + describe.skip('dashboard saved search embeddable', () => { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index bb75b4441f8802..245b895d75b3a5 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -38,7 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('query', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104409 + describe.skip('query', function () { const queryName1 = 'Query # 1'; it('should show correct time range string by timepicker', async function () { diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 20f2cab907d9bf..29073c5fe4ebb4 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('saved query management component functionality', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104366 + describe.skip('saved query management component functionality', function () { before(async function () { // set up a query with filters and a time filter log.debug('set up a query with filters to save'); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 1d4d4fee0175eb..ca310493960f53 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -125,7 +125,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('switch index patterns', () => { + // FLAKY: https://github.com/elastic/kibana/issues/103252 + describe.skip('switch index patterns', () => { before(async () => { await esArchiver.loadIfNeeded( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 7f1ea64bcd9792..ea11560e37b6ff 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -447,7 +447,9 @@ export class VisualBuilderPageObject extends FtrService { const metricsIndexPatternInput = 'metricsIndexPatternInput'; if (useKibanaIndices !== undefined) { - await this.switchIndexPatternSelectionMode(useKibanaIndices); + await this.retry.try(async () => { + await this.switchIndexPatternSelectionMode(useKibanaIndices); + }); } if (useKibanaIndices === false) { 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 cf720657291f88..b03451bdebad2d 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 @@ -9,7 +9,7 @@ import './main.scss'; import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; -import { first } from 'rxjs/operators'; +import { first, pluck } from 'rxjs/operators'; import { IInterpreterRenderHandlers, ExpressionValue, @@ -59,7 +59,9 @@ class Main extends React.Component<{}, State> { inspectorAdapters: adapters, searchContext: initialContext as any, }) - .getData(); + .getData() + .pipe(pluck('result')) + .toPromise(); }; let lastRenderHandler: ExpressionRenderHandler; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/server/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/server/plugin.ts index 923113cc99ba8f..a4903fedd22d59 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/server/plugin.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/server/plugin.ts @@ -7,6 +7,7 @@ */ import { schema } from '@kbn/config-schema'; +import { pluck } from 'rxjs/operators'; import { CoreSetup, Plugin, HttpResponsePayload } from '../../../../../src/core/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import { ExpressionsServerStart } from '../../../../../src/plugins/expressions/server'; @@ -32,13 +33,12 @@ export class TestPlugin implements Plugin { const [, { expressions }] = await core.getStartServices(); - const output = await expressions.run( - req.body.expression, - req.body.input, - { + const output = await expressions + .run(req.body.expression, req.body.input, { kibanaRequest: req, - } - ); + }) + .pipe(pluck('result')) + .toPromise(); return res.ok({ body: output }); } ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts index 25090e14ebf913..3912b60dd56ed1 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts @@ -9,23 +9,28 @@ import Path from 'path'; const ES_ARCHIVE_DIR = './cypress/fixtures/es_archiver'; +// Otherwise cy.exec would inject NODE_TLS_REJECT_UNAUTHORIZED=0 and node would abort if used over https +const NODE_TLS_REJECT_UNAUTHORIZED = '1'; + export const esArchiverLoad = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); cy.exec( - `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js` + `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`, + { env: { NODE_TLS_REJECT_UNAUTHORIZED } } ); }; export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); cy.exec( - `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js` + `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`, + { env: { NODE_TLS_REJECT_UNAUTHORIZED } } ); }; export const esArchiverResetKibana = () => { cy.exec( `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, - { failOnNonZeroExit: false } + { env: { NODE_TLS_REJECT_UNAUTHORIZED }, failOnNonZeroExit: false } ); }; diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 21aef379715c7d..ae4510b10acd44 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -3,39 +3,34 @@ "version": "8.0.0", "kibanaVersion": "kibana", "requiredPlugins": [ - "features", "apmOss", "data", - "licensing", - "triggersActionsUi", "embeddable", + "features", + "fleet", "infra", + "licensing", "observability", - "ruleRegistry" + "ruleRegistry", + "triggersActionsUi" ], "optionalPlugins": [ - "spaces", - "cloud", - "usageCollection", - "taskManager", "actions", "alerting", - "security", - "ml", + "cloud", "home", "maps", - "fleet" + "ml", + "security", + "spaces", + "taskManager", + "usageCollection" ], "server": true, "ui": true, - "configPath": [ - "xpack", - "apm" - ], - "extraPublicDirs": [ - "public/style/variables" - ], + "configPath": ["xpack", "apm"], "requiredBundles": [ + "fleet", "home", "kibanaReact", "kibanaUtils", @@ -43,4 +38,4 @@ "ml", "observability" ] -} \ No newline at end of file +} diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx index d069d4a11b4942..16b8cc34e97527 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx @@ -17,7 +17,7 @@ import { import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; export default { - title: 'app/TransactionDurationAlertTrigger', + title: 'alerting/TransactionDurationAlertTrigger', component: TransactionDurationAlertTrigger, decorators: [ (Story: ComponentType) => { @@ -26,7 +26,7 @@ export default { http: { get: (endpoint: string) => { if (endpoint === '/api/apm/environments') { - return Promise.resolve(['production']); + return Promise.resolve({ environments: ['production'] }); } else { return Promise.resolve({ transactionTypes: ['request'], diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/ConfirmDeleteModal.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/ConfirmDeleteModal.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index c098be41968dd2..93ae8b270b5de1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -22,13 +22,12 @@ import { getOptionLabel } from '../../../../../../common/agent_configuration/all import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { useTheme } from '../../../../../hooks/use_theme'; -import { px, units } from '../../../../../style/variables'; import { createAgentConfigurationHref, editAgentConfigurationHref, } from '../../../../shared/Links/apm/agentConfigurationLinks'; import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; -import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; +import { ITableColumn, ManagedTable } from '../../../../shared/managed_table'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; @@ -125,7 +124,7 @@ export function AgentConfigurationList({ { field: 'applied_by_agent', align: 'center', - width: px(units.double), + width: theme.eui.euiSizeXL, name: '', sortable: true, render: (isApplied: boolean) => ( @@ -190,7 +189,7 @@ export function AgentConfigurationList({ ...(canSave ? [ { - width: px(units.double), + width: theme.eui.euiSizeXL, name: '', render: (config: Config) => ( ( { +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +describe('DeleteButton', () => { beforeAll(() => { jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); }); + it('deletes a custom link', async () => { const onDeleteMock = jest.fn(); const { getByText } = render( - - - + , + { wrapper: Wrapper } ); + await act(async () => { fireEvent.click(getByText('Delete')); }); + expect(onDeleteMock).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/DeleteButton.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/DeleteButton.tsx index eb6dfdd763ac4e..c6547aaff06710 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/DeleteButton.tsx @@ -9,9 +9,9 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; import React, { useState } from 'react'; -import { px, unit } from '../../../../../../style/variables'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; +import { useTheme } from '../../../../../../hooks/use_theme'; interface Props { onDelete: () => void; @@ -21,6 +21,7 @@ interface Props { export function DeleteButton({ onDelete, customLinkId }: Props) { const [isDeleting, setIsDeleting] = useState(false); const { toasts } = useApmPluginContext().core.notifications; + const theme = useTheme(); return ( {i18n.translate('xpack.apm.settings.customizeUI.customLink.delete', { defaultMessage: 'Delete', diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/Documentation.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/FiltersSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FiltersSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/FiltersSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/FlyoutFooter.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/FlyoutFooter.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/FlyoutFooter.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/LinkSection.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/LinkSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper.test.ts similarity index 98% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper.test.ts index c4420f3a59c813..449968b82943fa 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.test.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper.test.ts @@ -8,7 +8,7 @@ import { getSelectOptions, replaceTemplateVariables, -} from '../CreateEditCustomLinkFlyout/helper'; +} from '../create_edit_custom_link_flyout/helper'; import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; describe('Custom link helper', () => { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper.ts rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/link_preview.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/saveCustomLink.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/saveCustomLink.ts rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/saveCustomLink.ts diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx index bb948ea665b1e7..9593802407193d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx @@ -5,22 +5,21 @@ * 2.0. */ -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiText, EuiSpacer, + EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; +import React, { useState } from 'react'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; -import { units, px } from '../../../../../style/variables'; -import { ManagedTable } from '../../../../shared/ManagedTable'; -import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; +import { ManagedTable } from '../../../../shared/managed_table'; +import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; interface Props { items: CustomLink[]; @@ -50,7 +49,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { truncateText: true, }, { - width: px(160), + width: 160, align: 'right', field: '@timestamp', name: i18n.translate( @@ -63,7 +62,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { ), }, { - width: px(units.triple), + width: '48px', name: '', actions: [ ...(canSave diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.test.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.test.tsx index 7d119b8c406da2..22fbfb04734bae 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.test.tsx @@ -22,7 +22,7 @@ import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; -import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; +import * as saveCustomLink from './create_edit_custom_link_flyout/saveCustomLink'; const data = { customLinks: [ diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.tsx index c1315f165abdb2..beea1d8276846c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/index.tsx @@ -8,21 +8,21 @@ import { EuiFlexGroup, EuiFlexItem, - EuiTitle, - EuiText, EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; -import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; import { useLicenseContext } from '../../../../../context/license/use_license_context'; +import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; import { LicensePrompt } from '../../../../shared/license_prompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; -import { CustomLinkTable } from './CustomLinkTable'; +import { CreateEditCustomLinkFlyout } from './create_edit_custom_link_flyout'; +import { CustomLinkTable } from './custom_link_table'; import { EmptyPrompt } from './EmptyPrompt'; export function CustomLinkOverview() { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/index.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/customize_ui/index.tsx index 9ce1f1325bb2c4..9d3ee3fa47c1ac 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { CustomLinkOverview } from './CustomLink'; +import { CustomLinkOverview } from './custom_link'; export function CustomizeUI() { return ; diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 526aad56e743ec..f0d7f8d60eb6ca 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -6,33 +6,32 @@ */ import { - ScaleType, - Chart, - LineSeries, Axis, + Chart, CurveType, + LineSeries, Position, - timeFormatter, + ScaleType, Settings, + timeFormatter, } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { useUiTracker } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { px } from '../../../style/variables'; +import { ChartContainer } from '../../shared/charts/chart_container'; import { CorrelationsTable, SelectedSignificantTerm, } from './correlations_table'; -import { ChartContainer } from '../../shared/charts/chart_container'; -import { useTheme } from '../../../hooks/use_theme'; import { CustomFields } from './custom_fields'; import { useFieldNames } from './use_field_names'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useUiTracker } from '../../../../../observability/public'; type OverallErrorsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'> @@ -221,7 +220,7 @@ function ErrorTimeseriesChart({ return ( - + theme.eui.euiSize}; `; const TransactionLinkName = euiStyled.div` - margin-left: ${px(units.half)}; + margin-left: ${({ theme }) => theme.eui.euiSizeS}; display: inline-block; vertical-align: middle; `; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 3d22c3863c1007..344393d42506fe 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -19,32 +19,31 @@ import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -import { useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; -import { DetailView } from './DetailView'; -import { ErrorDistribution } from './Distribution'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { DetailView } from './detail_view'; +import { ErrorDistribution } from './Distribution'; const Titles = euiStyled.div` - margin-bottom: ${px(units.plus)}; + margin-bottom: ${({ theme }) => theme.eui.euiSizeL}; `; const Label = euiStyled.div` - margin-bottom: ${px(units.quarter)}; - font-size: ${fontSizes.small}; + margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; color: ${({ theme }) => theme.eui.euiColorDarkShade}; `; const Message = euiStyled.div` - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; font-weight: bold; - font-size: ${fontSizes.large}; - margin-bottom: ${px(units.half)}; + font-size: ${({ theme }) => theme.eui.euiFontSizeM}; + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; `; const Culprit = euiStyled.div` - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; `; function getShortGroupId(errorGroupId?: string) { diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx index e368230fbc77e4..a2a92b7e16f8ea 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx @@ -13,6 +13,7 @@ import { mockMoment, toJson } from '../../../../utils/testHelpers'; import { ErrorGroupList } from './index'; import props from './__fixtures__/props.json'; import { MemoryRouter } from 'react-router-dom'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -41,13 +42,18 @@ describe('ErrorGroupOverview -> List', () => { it('should render with data', () => { const wrapper = mount( - - - - - - - + + + + + + + + + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap index 92337a97573e45..2b85d6bb3c229c 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap @@ -268,7 +268,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` exports[`ErrorGroupOverview -> List should render with data 1`] = ` .c0 { - font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; + font-family: 'Roboto Mono','Consolas','Menlo','Courier',monospace; } .c2 { @@ -286,8 +286,8 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c3 { - font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; - font-size: 16px; + font-family: 'Roboto Mono','Consolas','Menlo','Courier',monospace; + font-size: 18px; max-width: 100%; white-space: nowrap; overflow: hidden; @@ -295,7 +295,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c4 { - font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; + font-family: 'Roboto Mono','Consolas','Menlo','Courier',monospace; }
theme.eui.euiCodeFontFamily}; `; const MessageAndCulpritCell = euiStyled.div` @@ -40,13 +33,13 @@ const ErrorLink = euiStyled(ErrorOverviewLink)` `; const MessageLink = euiStyled(ErrorDetailLink)` - font-family: ${fontFamilyCode}; - font-size: ${fontSizes.large}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + font-size: ${({ theme }) => theme.eui.euiFontSizeM}; ${truncate('100%')}; `; const Culprit = euiStyled.div` - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; `; type ErrorGroupItem = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>['errorGroups'][0]; @@ -86,7 +79,7 @@ function ErrorGroupList({ items, serviceName }: Props) { ), field: 'groupId', sortable: false, - width: px(unit * 6), + width: unit * 6, render: (groupId: string) => { return ( diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 886ef8412f35b9..4c622758e6c8bf 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -18,7 +18,7 @@ import { useTrackPageview } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; +import { ErrorDistribution } from '../error_group_details/Distribution'; import { ErrorGroupList } from './List'; interface ErrorGroupOverviewProps { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index e4a0ef3ef09f1b..cac94885511c10 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -18,8 +18,8 @@ import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { NoServicesMessage } from './no_services_message'; -import { ServiceList } from './ServiceList'; -import { MLCallout } from './ServiceList/MLCallout'; +import { ServiceList } from './service_list'; +import { MLCallout } from './service_list/MLCallout'; const initialData = { items: [], diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/HealthBadge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/HealthBadge.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/MLCallout.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/service_api_mock_data.ts b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/__fixtures__/service_api_mock_data.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/__fixtures__/service_api_mock_data.ts rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/__fixtures__/service_api_mock_data.ts diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 6c40639594adf5..b4644068fd782a 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -5,31 +5,35 @@ * 2.0. */ -import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useMemo } from 'react'; import { ValuesType } from 'utility-types'; -import { orderBy } from 'lodash'; -import { EuiIcon } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../../../common/transaction_types'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { - asPercent, asMillisecondDuration, + asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { fontSizes, px, truncate, unit } from '../../../../style/variables'; -import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { truncate, unit } from '../../../../utils/style'; +import { AgentIcon } from '../../../shared/agent_icon'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview_link'; -import { AgentIcon } from '../../../shared/agent_icon'; +import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; @@ -47,7 +51,7 @@ function formatString(value?: string | null) { } const AppLink = euiStyled(ServiceOrTransactionsOverviewLink)` - font-size: ${fontSizes.large}; + font-size: ${({ theme }) => theme.eui.euiFontSizeM} ${truncate('100%')}; `; @@ -80,7 +84,7 @@ export function getServiceColumns({ name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { defaultMessage: 'Health', }), - width: px(unit * 6), + width: unit * 6, sortable: true, render: (_, { healthStatus }) => { return ( @@ -130,7 +134,7 @@ export function getServiceColumns({ name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { defaultMessage: 'Environment', }), - width: px(unit * 10), + width: unit * 10, sortable: true, render: (_, { environments }) => ( @@ -144,7 +148,7 @@ export function getServiceColumns({ 'xpack.apm.servicesTable.transactionColumnLabel', { defaultMessage: 'Transaction type' } ), - width: px(unit * 10), + width: unit * 10, sortable: true, }, ] @@ -164,7 +168,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: px(unit * 10), + width: unit * 10, }, { field: 'transactionsPerMinute', @@ -181,7 +185,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: px(unit * 10), + width: unit * 10, }, { field: 'transactionErrorRate', @@ -204,7 +208,7 @@ export function getServiceColumns({ ); }, align: 'left', - width: px(unit * 10), + width: unit * 10, }, ]; } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx index fe3922060533a9..6b7626514d03fb 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx @@ -17,7 +17,7 @@ import { Popover } from '.'; import exampleGroupedConnectionsData from '../__stories__/example_grouped_connections.json'; export default { - title: 'app/service_map/Popover', + title: 'app/ServiceMap/Popover', component: Popover, decorators: [ (Story: ComponentType) => { diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/ServiceStatsFetcher.tsx index a71f299ab296c8..3155a65b06aca9 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/ServiceStatsFetcher.tsx @@ -18,7 +18,7 @@ import { ServiceNodeStats } from '../../../../../common/service_map'; import { ServiceStatsList } from './ServiceStatsList'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { AnomalyDetection } from './AnomalyDetection'; +import { AnomalyDetection } from './anomaly_detection'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; interface ServiceStatsFetcherProps { diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/service_map/Popover/AnomalyDetection.tsx rename to x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx index c98116a69da667..1ceb90ff838ad1 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/anomaly_detection.tsx @@ -5,30 +5,29 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiTitle, - EuiIconTip, EuiHealth, + EuiIconTip, + EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { + getSeverity, + ServiceAnomalyStats, +} from '../../../../../common/anomaly_detection'; import { getServiceHealthStatus, getServiceHealthStatusColor, } from '../../../../../common/service_health_status'; +import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; +import { asDuration, asInteger } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; -import { fontSize, px } from '../../../../style/variables'; -import { asInteger, asDuration } from '../../../../../common/utils/formatters'; import { MLSingleMetricLink } from '../../../shared/Links/MachineLearningLinks/MLSingleMetricLink'; import { popoverWidth } from '../cytoscape_options'; -import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; -import { - getSeverity, - ServiceAnomalyStats, -} from '../../../../../common/anomaly_detection'; const HealthStatusTitle = euiStyled(EuiTitle)` display: inline; @@ -47,8 +46,8 @@ const SubduedText = euiStyled.span` const EnableText = euiStyled.section` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; line-height: 1.4; - font-size: ${fontSize}; - width: ${px(popoverWidth)}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + width: ${popoverWidth}px; `; export const ContentLine = euiStyled.section` diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx index 83f0a3ea7e4b9e..a8f004a7295d90 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx @@ -10,7 +10,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea import { ServiceStatsList } from './ServiceStatsList'; export default { - title: 'app/service_map/Popover/ServiceStatsList', + title: 'app/ServiceMap/Popover/ServiceStatsList', component: ServiceStatsList, decorators: [ (Story: ComponentType) => ( diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx index dbab10d7b93b60..8bc0d7239e9c52 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx @@ -12,7 +12,7 @@ import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; export default { - title: 'app/service_map/Cytoscape', + title: 'app/ServiceMap/Cytoscape', component: Cytoscape, decorators: [ (Story: ComponentType) => ( diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx index 84351d5716edb2..45de632a152d41 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx @@ -25,7 +25,7 @@ import exampleResponseOpbeansBeats from './example_response_opbeans_beats.json'; import exampleResponseTodo from './example_response_todo.json'; import { generateServiceMapElements } from './generate_service_map_elements'; -const STORYBOOK_PATH = 'app/service_map/Cytoscape/Example data'; +const STORYBOOK_PATH = 'app/ServiceMap/Example data'; const SESSION_STORAGE_KEY = `${STORYBOOK_PATH}/pre-loaded map`; function getSessionJson() { @@ -40,7 +40,7 @@ function getHeight() { } export default { - title: 'app/service_map/Cytoscape/Example data', + title: 'app/ServiceMap/Example data', component: Cytoscape, decorators: [ (Story: ComponentType) => ( diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index a147528d42caec..07afcbc9c4682f 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -20,12 +20,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { px, truncate, unit } from '../../../style/variables'; +import { truncate, unit } from '../../../utils/style'; import { MetricsChart } from '../../shared/charts/metrics_chart'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; @@ -36,7 +36,7 @@ const INITIAL_DATA = { const Truncate = euiStyled.span` display: block; - ${truncate(px(unit * 12))} + ${truncate(unit * 12)} `; interface ServiceNodeMetricsProps { diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 1a432f90f1e3a4..58541e2c5501b5 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -19,16 +19,16 @@ import { } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../hooks/use_fetcher'; -import { px, truncate, unit } from '../../../style/variables'; +import { truncate, unit } from '../../../utils/style'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; -import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; +import { ITableColumn, ManagedTable } from '../../shared/managed_table'; const INITIAL_PAGE_SIZE = 25; const INITIAL_SORT_FIELD = 'cpu'; const INITIAL_SORT_DIRECTION = 'desc'; const ServiceNodeName = euiStyled.div` - ${truncate(px(8 * unit))} + ${truncate(8 * unit)} `; interface ServiceNodeOverviewProps { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index b1a4d5ca5fda78..0067558865bd6d 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -15,19 +15,19 @@ import { import { i18n } from '@kbn/i18n'; import { keyBy } from 'lodash'; import React from 'react'; -import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/offset_previous_period_coordinate'; -import { Coordinate } from '../../../../../typings/timeseries'; import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; import { asMillisecondDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; +import { offsetPreviousPeriodCoordinates } from '../../../../../common/utils/offset_previous_period_coordinate'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies'; +import { Coordinate } from '../../../../../typings/timeseries'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { AgentIcon } from '../../../shared/agent_icon'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; @@ -156,7 +156,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { defaultMessage: 'Latency (avg.)', } ), - width: px(unit * 10), + width: `${unit * 10}px`, render: (_, { latency }) => { return ( { return ( { return ( { return ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx index 4ad83f7d87426c..b458f6147b3f11 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -9,12 +9,12 @@ import { EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { asInteger } from '../../../../../common/utils/formatters'; -import { px, unit } from '../../../../style/variables'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { unit } from '../../../../utils/style'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; type ErrorGroupMainStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/main_statistics'>; type ErrorGroupDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/detailed_statistics'>; @@ -61,7 +61,7 @@ export function getColumns({ render: (_, { last_seen: lastSeen }) => { return ; }, - width: px(unit * 9), + width: `${unit * 9}px`, }, { field: 'occurrences', @@ -71,7 +71,7 @@ export function getColumns({ defaultMessage: 'Occurrences', } ), - width: px(unit * 12), + width: `${unit * 12}px`, render: (_, { occurrences, group_id: errorGroupId }) => { const currentPeriodTimeseries = errorGroupDetailedStatistics?.currentPeriod?.[errorGroupId] diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index a92efff1039104..f9600b9d7f4183 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -25,14 +25,14 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { px, unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink'; import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { getLatencyColumnLabel } from '../get_latency_column_label'; -import { InstanceActionsMenu } from './instance_actions_menu'; import { MainStatsServiceInstanceItem } from '../service_overview_instances_chart_and_table'; +import { InstanceActionsMenu } from './instance_actions_menu'; type ServiceInstanceDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics'>; @@ -98,7 +98,7 @@ export function getColumns({ { field: 'latency', name: getLatencyColumnLabel(latencyAggregationType), - width: px(unit * 10), + width: `${unit * 10}px`, render: (_, { serviceNodeName, latency }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.latency; @@ -123,7 +123,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.instancesTableColumnThroughput', { defaultMessage: 'Throughput' } ), - width: px(unit * 10), + width: `${unit * 10}px`, render: (_, { serviceNodeName, throughput }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.throughput; @@ -149,7 +149,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.instancesTableColumnErrorRate', { defaultMessage: 'Error rate' } ), - width: px(unit * 8), + width: `${unit * 8}px`, render: (_, { serviceNodeName, errorRate }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.errorRate; @@ -175,7 +175,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.instancesTableColumnCpuUsage', { defaultMessage: 'CPU usage (avg.)' } ), - width: px(unit * 8), + width: `${unit * 8}px`, render: (_, { serviceNodeName, cpuUsage }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.cpuUsage; @@ -201,7 +201,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.instancesTableColumnMemoryUsage', { defaultMessage: 'Memory usage (avg.)' } ), - width: px(unit * 9), + width: `${unit * 9}px`, render: (_, { serviceNodeName, memoryUsage }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.memoryUsage; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx index f03c2b2fc9091a..a2aaa61e8a6614 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx @@ -20,8 +20,7 @@ import { SERVICE_NODE_NAME } from '../../../../../../common/elasticsearch_fieldn import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; -import { px } from '../../../../../style/variables'; -import { pushNewItemToKueryBar } from '../../../../shared/KueryBar/utils'; +import { pushNewItemToKueryBar } from '../../../../shared/kuery_bar/utils'; import { useMetricOverviewHref } from '../../../../shared/Links/apm/MetricOverviewLink'; import { useServiceNodeMetricOverviewHref } from '../../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { useInstanceDetailsFetcher } from '../use_instance_details_fetcher'; @@ -33,7 +32,7 @@ interface Props { onClose: () => void; } -const POPOVER_WIDTH = px(305); +const POPOVER_WIDTH = '305px'; export function InstanceActionsMenu({ serviceName, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts index 30995fbd133977..0e78e44eedf77b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts @@ -7,30 +7,17 @@ import { i18n } from '@kbn/i18n'; import { IBasePath } from 'kibana/public'; -import { isEmpty } from 'lodash'; import moment from 'moment'; import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getInfraHref } from '../../../../shared/Links/InfraLink'; +import { + Action, + getNonEmptySections, + SectionRecord, +} from '../../../../shared/transaction_action_menu/sections_helper'; type InstaceDetails = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; -interface Action { - key: string; - label: string; - href?: string; - onClick?: () => void; - condition: boolean; -} - -interface Section { - key: string; - title?: string; - subtitle?: string; - actions: Action[]; -} - -type SectionRecord = Record; - function getInfraMetricsQuery(timestamp?: string) { if (!timestamp) { return { from: 0, to: 0 }; @@ -189,15 +176,5 @@ export function getMenuSections({ apm: [{ key: 'apm', actions: apmActions }], }; - // Filter out actions that shouldnt be shown and sections without any actions. - return Object.values(sectionRecord) - .map((sections) => - sections - .map((section) => ({ - ...section, - actions: section.actions.filter((action) => action.condition), - })) - .filter((section) => !isEmpty(section.actions)) - ) - .filter((sections) => !isEmpty(sections)); + return getNonEmptySections(sectionRecord); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/intance_details.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/intance_details.tsx index 35cecc49e293b7..0c77051bea293e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/intance_details.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/intance_details.tsx @@ -28,10 +28,9 @@ import { useUrlParams } from '../../../../context/url_params_context/use_url_par import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { pct } from '../../../../style/variables'; import { getAgentIcon } from '../../../shared/agent_icon/get_agent_icon'; import { KeyValueFilterList } from '../../../shared/key_value_filter_list'; -import { pushNewItemToKueryBar } from '../../../shared/KueryBar/utils'; +import { pushNewItemToKueryBar } from '../../../shared/kuery_bar/utils'; import { getCloudIcon, getContainerIcon } from '../../../shared/service_icons'; import { useInstanceDetailsFetcher } from './use_instance_details_fetcher'; @@ -78,7 +77,7 @@ export function InstanceDetails({ serviceName, serviceNodeName }: Props) { status === FETCH_STATUS.NOT_INITIATED ) { return ( -
+
); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx index 9ac1c7d64d8b23..2e46e23ccaa42e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -16,7 +16,7 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { px, unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; @@ -71,7 +71,7 @@ export function getColumns({ field: 'latency', sortable: true, name: getLatencyColumnLabel(latencyAggregationType), - width: px(unit * 10), + width: `${unit * 10}px`, render: (_, { latency, name }) => { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.latency; @@ -97,7 +97,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.transactionsTableColumnThroughput', { defaultMessage: 'Throughput' } ), - width: px(unit * 10), + width: `${unit * 10}px`, render: (_, { throughput, name }) => { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.throughput; @@ -124,7 +124,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', { defaultMessage: 'Error rate' } ), - width: px(unit * 8), + width: `${unit * 8}px`, render: (_, { errorRate, name }) => { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.errorRate; @@ -150,7 +150,7 @@ export function getColumns({ 'xpack.apm.serviceOverview.transactionsTableColumnImpact', { defaultMessage: 'Impact' } ), - width: px(unit * 5), + width: `${unit * 5}px`, render: (_, { name }) => { const currentImpact = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.impact ?? diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index 1adf58d0394c60..82ac3a04f63f14 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -13,16 +13,16 @@ import { Settings, TooltipInfo, } from '@elastic/charts'; -import { EuiInMemoryTable } from '@elastic/eui'; -import { EuiFieldText } from '@elastic/eui'; -import { EuiToolTip } from '@elastic/eui'; import { EuiCheckbox, + EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiInMemoryTable, euiPaletteForTemperature, EuiText, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { find, sumBy } from 'lodash'; @@ -44,7 +44,7 @@ import { } from '../../../../common/utils/formatters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; -import { px, unit } from '../../../style/variables'; +import { unit } from '../../../utils/style'; const colors = euiPaletteForTemperature(100).slice(50, 85); @@ -335,7 +335,7 @@ export function ServiceProfilingFlamegraph({ /> - + formatValue(item.value, valueUnit), - width: px(unit * 6), + width: `${unit * 6}px`, }, ]} /> diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index bf60463255d64c..d280b36a603bab 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -11,7 +11,7 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; -import { TraceList } from './TraceList'; +import { TraceList } from './trace_list'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx rename to x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx index 774333c35b479e..f1c8df553abf71 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx @@ -13,18 +13,18 @@ import { asMillisecondDuration, asTransactionRate, } from '../../../../common/utils/formatters'; -import { fontSizes, truncate } from '../../../style/variables'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { truncate } from '../../../utils/style'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; -import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; import { TransactionDetailLink } from '../../shared/Links/apm/transaction_detail_link'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; +import { ITableColumn, ManagedTable } from '../../shared/managed_table'; type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; const StyledTransactionLink = euiStyled(TransactionDetailLink)` - font-size: ${fontSizes.large}; + font-size: ${({ theme }) => theme.eui.euiFontSizeM}; ${truncate('100%')}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx index c7dae6ce3d1d43..f59b3ddab7c055 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx @@ -29,7 +29,7 @@ import type { IUrlParams } from '../../../../context/url_params_context/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { CustomTooltip } from './custom_tooltip'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx deleted file mode 100644 index 3584309ebb20c9..00000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; -import { - borderRadius, - fontFamilyCode, - fontSize, - px, - unit, - units, -} from '../../../../../../../style/variables'; -import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; - -const ContextUrl = euiStyled.div` - padding: ${px(units.half)} ${px(unit)}; - background: ${({ theme }) => theme.eui.euiColorLightestShade}; - border-radius: ${borderRadius}; - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - font-family: ${fontFamilyCode}; - font-size: ${fontSize}; -`; - -interface Props { - httpContext: NonNullable['http']; -} - -export function HttpContext({ httpContext }: Props) { - const url = httpContext?.url?.original; - - if (!url) { - return null; - } - - return ( - - -

HTTP URL

-
- - {url} - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 3cac05ba2d96a8..1e13e224a511a4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -19,7 +19,7 @@ import { HeightRetainer } from '../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { TransactionDistribution } from './Distribution'; import { useWaterfallFetcher } from './use_waterfall_fetcher'; -import { WaterfallWithSummmary } from './WaterfallWithSummmary'; +import { WaterfallWithSummary } from './waterfall_with_summary'; interface Sample { traceId: string; @@ -107,7 +107,7 @@ export function TransactionDetails() { - ; @@ -39,7 +39,7 @@ interface Props { traceSamples: DistributionBucket['samples']; } -export function WaterfallWithSummmary({ +export function WaterfallWithSummary({ urlParams, waterfall, exceedsMax, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.test.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.test.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/index.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Marks/index.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx index 5f4ed551597ed6..0ad0fe872a8408 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx @@ -16,7 +16,7 @@ import { import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview_link'; import { TransactionDetailLink } from '../../../../../shared/Links/apm/transaction_detail_link'; -import { StickyProperties } from '../../../../../shared/StickyProperties'; +import { StickyProperties } from '../../../../../shared/sticky_properties'; interface Props { transaction?: Transaction; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/ResponsiveFlyout.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/ResponsiveFlyout.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/index.tsx index 680edf880a70ec..6468e6ed1e2c8c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/TransactionFlyout/index.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu'; +import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/TransactionActionMenu'; import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary'; import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties'; import { ResponsiveFlyout } from '../ResponsiveFlyout'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx index fddf4fcb4efc97..ec6d550affb91b 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/WaterfallFlyout.tsx @@ -8,7 +8,7 @@ import { History } from 'history'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { SpanFlyout } from './SpanFlyout'; +import { SpanFlyout } from './span_flyout'; import { TransactionFlyout } from './TransactionFlyout'; import { IWaterfall } from './waterfall_helpers/waterfall_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx index b0721791081fac..1935d373caf797 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import React, { useState } from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; -import { WaterfallItem } from './WaterfallItem'; +import { WaterfallItem } from './waterfall_item'; import { IWaterfall, IWaterfallSpanOrTransaction, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx index 60d328c98a4b7f..9e1174818c43be 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/StickySpanProperties.tsx @@ -19,7 +19,7 @@ import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview_link'; import { TransactionDetailLink } from '../../../../../../shared/Links/apm/transaction_detail_link'; -import { StickyProperties } from '../../../../../../shared/StickyProperties'; +import { StickyProperties } from '../../../../../../shared/sticky_properties'; interface Props { span: Span; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/database_context.tsx similarity index 76% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/database_context.tsx index 6fd6873cf565ef..42792057434429 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/database_context.tsx @@ -9,39 +9,35 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { tint } from 'polished'; import React, { Fragment } from 'react'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; -import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -import { - borderRadius, - fontFamilyCode, - fontSize, - px, - unit, - units, -} from '../../../../../../../style/variables'; -import { TruncateHeightSection } from './TruncateHeightSection'; +import { useTheme } from '../../../../../../../hooks/use_theme'; +import { TruncateHeightSection } from './truncate_height_section'; SyntaxHighlighter.registerLanguage('sql', sql); const DatabaseStatement = euiStyled.div` - padding: ${px(units.half)} ${px(unit)}; + padding: ${({ theme }) => + `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.m}`}; background: ${({ theme }) => tint(0.9, theme.eui.euiColorWarning)}; - border-radius: ${borderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - font-family: ${fontFamilyCode}; - font-size: ${fontSize}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; `; -const dbSyntaxLineHeight = unit * 1.5; - interface Props { dbContext?: NonNullable['db']; } export function DatabaseContext({ dbContext }: Props) { + const theme = useTheme(); + const dbSyntaxLineHeight = theme.eui.euiSizeL; + const previewHeight = 240; // 10 * dbSyntaxLineHeight + if (!dbContext || !dbContext.statement) { return null; } @@ -64,7 +60,7 @@ export function DatabaseContext({ dbContext }: Props) { - + theme.eui.euiSizeXS}; `; const HttpInfoContainer = euiStyled('div')` - margin-right: ${px(units.quarter)}; + margin-right: ${({ theme }) => theme.eui.euiSizeXS}; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/truncate_height_section.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/truncate_height_section.tsx index 181fcb91ba3e6e..4c845e16348e77 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/truncate_height_section.tsx @@ -9,10 +9,9 @@ import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment, ReactNode, useEffect, useRef, useState } from 'react'; import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; -import { px, units } from '../../../../../../../style/variables'; const ToggleButtonContainer = euiStyled.div` - margin-top: ${px(units.half)}; + margin-top: ${({ theme }) => theme.eui.euiSizeS} user-select: none; `; @@ -41,7 +40,7 @@ export function TruncateHeightSection({ children, previewHeight }: Props) { ref={contentContainerEl} style={{ overflow: 'hidden', - maxHeight: isOpen ? 'initial' : px(previewHeight), + maxHeight: isOpen ? 'initial' : previewHeight, }} > {children} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.stories.tsx similarity index 91% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.stories.tsx index 8275aa1e5f156c..6b52fbe2d784aa 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.stories.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { SyncBadge, SyncBadgeProps } from './SyncBadge'; +import { SyncBadge, SyncBadgeProps } from './sync_badge'; export default { title: 'app/TransactionDetails/SyncBadge', diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx index b9e4c6951fa06a..cfc369fa12a269 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx @@ -9,11 +9,10 @@ import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import { px, units } from '../../../../../../style/variables'; const SpanBadge = euiStyled(EuiBadge)` display: inline-block; - margin-right: ${px(units.quarter)}; + margin-right: ${({ theme }) => theme.eui.euiSizeXS}; `; export interface SyncBadgeProps { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx index f3e1547c4b8b87..a2c2c869a079c2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx @@ -5,20 +5,18 @@ * 2.0. */ -import React, { ReactNode } from 'react'; - import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React, { ReactNode } from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import { asDuration } from '../../../../../../../common/utils/formatters'; import { isRumAgentName } from '../../../../../../../common/agent_name'; -import { px, unit, units } from '../../../../../../style/variables'; -import { ErrorCount } from '../../ErrorCount'; -import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; -import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; -import { SyncBadge } from './SyncBadge'; +import { asDuration } from '../../../../../../../common/utils/formatters'; import { Margins } from '../../../../../shared/charts/Timeline'; +import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; +import { ErrorCount } from '../../ErrorCount'; +import { SyncBadge } from './sync_badge'; +import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; type ItemType = 'transaction' | 'span' | 'error'; @@ -37,10 +35,10 @@ const Container = euiStyled.div` position: relative; display: block; user-select: none; - padding-top: ${px(units.half)}; - padding-bottom: ${px(units.plus)}; - margin-right: ${(props) => px(props.timelineMargins.right)}; - margin-left: ${(props) => px(props.timelineMargins.left)}; + padding-top: ${({ theme }) => theme.eui.paddingSizes.s}; + padding-bottom: ${({ theme }) => theme.eui.euiSizeM}; + margin-right: ${(props) => props.timelineMargins.right}px; + margin-left: ${(props) => props.timelineMargins.left}px; background-color: ${({ isSelected, theme }) => isSelected ? theme.eui.euiColorLightestShade : 'initial'}; cursor: pointer; @@ -53,7 +51,7 @@ const Container = euiStyled.div` const ItemBar = euiStyled.div` box-sizing: border-box; position: relative; - height: ${px(unit)}; + height: ${({ theme }) => theme.eui.euiSize}; min-width: 2px; background-color: ${(props) => props.color}; `; @@ -63,11 +61,11 @@ const ItemText = euiStyled.span` right: 0; display: flex; align-items: center; - height: ${px(units.plus)}; + height: ${({ theme }) => theme.eui.euiSizeL}; /* add margin to all direct descendants */ & > * { - margin-right: ${px(units.half)}; + margin-right: ${({ theme }) => theme.eui.euiSizeS}; white-space: nowrap; } `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallLegends.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx index d6f6de2d9179b5..aaa9b3e45ee22f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallLegends.tsx @@ -10,7 +10,7 @@ import { EuiFlexItem } from '@elastic/eui'; import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Legend } from '../../../../shared/charts/Legend'; +import { Legend } from '../../../../shared/charts/Timeline/legend'; import { IWaterfallLegend, WaterfallLegendType, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 4f0f92cafa5e7d..041c12822357c5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -24,7 +24,7 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { TransactionList } from './TransactionList'; +import { TransactionList } from './transaction_list'; import { useRedirect } from './useRedirect'; import { useTransactionListFetcher } from './use_transaction_list'; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_list/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx rename to x-pack/plugins/apm/public/components/app/transaction_overview/transaction_list/index.tsx index 795a6e66f70a46..dc3bf924d6fdce 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_list/index.tsx @@ -15,9 +15,9 @@ import { asMillisecondDuration, asTransactionRate, } from '../../../../../common/utils/formatters'; -import { fontFamilyCode, truncate } from '../../../../style/variables'; +import { truncate } from '../../../../utils/style'; import { ImpactBar } from '../../../shared/ImpactBar'; -import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; +import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; @@ -27,7 +27,7 @@ type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/trans // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. const TransactionNameLink = euiStyled(TransactionDetailLink)` - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; white-space: nowrap; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_list/transaction_list.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx rename to x-pack/plugins/apm/public/components/app/transaction_overview/transaction_list/transaction_list.stories.tsx diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_custom_assets_extension.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_custom_assets_extension.tsx new file mode 100644 index 00000000000000..40b902b7729d0e --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_custom_assets_extension.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + CustomAssetsAccordionProps, + CustomAssetsAccordion, +} from '../../../../fleet/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../../plugin'; + +export function ApmCustomAssetsExtension() { + const { http } = useKibana().services; + const basePath = http?.basePath.get(); + + const views: CustomAssetsAccordionProps['views'] = [ + { + name: i18n.translate('xpack.apm.fleetIntegration.assets.name', { + defaultMessage: 'Services', + }), + url: `${basePath}/app/apm`, + description: i18n.translate( + 'xpack.apm.fleetIntegration.assets.description', + { defaultMessage: 'View application traces and service maps in APM' } + ), + }, + ]; + + return ; +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_enrollment_flyout_extension.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_enrollment_flyout_extension.tsx new file mode 100644 index 00000000000000..6b3ba6a8f05dbd --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_enrollment_flyout_extension.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AgentEnrollmentFlyoutFinalStepExtension } from '../../../../fleet/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../../plugin'; + +function StepComponent() { + const { http } = useKibana().services; + const installApmAgentLink = http?.basePath.prepend('/app/home#/tutorial/apm'); + + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentDescription', + { + defaultMessage: + 'After the agent starts, you can install APM agents on your hosts to collect data from your applications and services.', + } + )} +

+
+ + + + {i18n.translate( + 'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentButtonText', + { defaultMessage: 'Install APM Agent' } + )} + + + ); +} + +export function getApmEnrollmentFlyoutData(): Pick< + AgentEnrollmentFlyoutFinalStepExtension, + 'title' | 'Component' +> { + return { + title: i18n.translate( + 'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentTitle', + { + defaultMessage: 'Install APM Agent', + } + ), + Component: StepComponent, + }; +} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/index.ts b/x-pack/plugins/apm/public/components/fleet_integration/index.ts similarity index 71% rename from x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/index.ts rename to x-pack/plugins/apm/public/components/fleet_integration/index.ts index 94203c2b156af2..a676a13c3741bd 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/index.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { ExperimentalBadge } from './experimental_badge'; +export * from './apm_enrollment_flyout_extension'; +export * from './lazy_apm_custom_assets_extension'; diff --git a/x-pack/plugins/apm/public/components/fleet_integration/lazy_apm_custom_assets_extension.tsx b/x-pack/plugins/apm/public/components/fleet_integration/lazy_apm_custom_assets_extension.tsx new file mode 100644 index 00000000000000..04f5042f3103f2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/lazy_apm_custom_assets_extension.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +export const LazyApmCustomAssetsExtension = lazy(async () => { + const { ApmCustomAssetsExtension } = await import( + './apm_custom_assets_extension' + ); + + return { + default: ApmCustomAssetsExtension, + }; +}); diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 5214489c9142b0..e00b7893b548ee 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -11,14 +11,14 @@ import { RouteComponentProps } from 'react-router-dom'; import { getServiceNodeName } from '../../../common/service_nodes'; import { APMRouteDefinition } from '../../application/routes'; import { toQuery } from '../shared/Links/url_helpers'; -import { ErrorGroupDetails } from '../app/ErrorGroupDetails'; +import { ErrorGroupDetails } from '../app/error_group_details'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { ServiceNodeMetrics } from '../app/service_node_metrics'; import { SettingsTemplate } from './templates/settings_template'; -import { AgentConfigurations } from '../app/Settings/AgentConfigurations'; +import { AgentConfigurations } from '../app/Settings/agent_configurations'; import { AnomalyDetection } from '../app/Settings/anomaly_detection'; import { ApmIndices } from '../app/Settings/ApmIndices'; -import { CustomizeUI } from '../app/Settings/CustomizeUI'; +import { CustomizeUI } from '../app/Settings/customize_ui'; import { Schema } from '../app/Settings/schema'; import { TraceLink } from '../app/TraceLink'; import { TransactionLink } from '../app/transaction_link'; @@ -37,7 +37,7 @@ import { TransactionOverview } from '../app/transaction_overview'; import { ServiceInventory } from '../app/service_inventory'; import { TraceOverview } from '../app/trace_overview'; import { useFetcher } from '../../hooks/use_fetcher'; -import { AgentConfigurationCreateEdit } from '../app/Settings/AgentConfigurations/AgentConfigurationCreateEdit'; +import { AgentConfigurationCreateEdit } from '../app/Settings/agent_configurations/AgentConfigurationCreateEdit'; // These component function definitions are used below with the `component` // property of the route definitions. diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index ef74894169d727..a03b0b5f2ae016 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -8,14 +8,13 @@ import { size } from 'lodash'; import { tint } from 'polished'; import React from 'react'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import javascript from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'; import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; import ruby from 'react-syntax-highlighter/dist/cjs/languages/hljs/ruby'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; -import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { borderRadius, px, unit, units } from '../../../style/variables'; SyntaxHighlighter.registerLanguage('javascript', javascript); SyntaxHighlighter.registerLanguage('python', python); @@ -23,15 +22,15 @@ SyntaxHighlighter.registerLanguage('ruby', ruby); const ContextContainer = euiStyled.div` position: relative; - border-radius: ${borderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; `; -const LINE_HEIGHT = units.eighth * 9; +const LINE_HEIGHT = 18; const LineHighlight = euiStyled.div<{ lineNumber: number }>` position: absolute; width: 100%; - height: ${px(units.eighth * 9)}; - top: ${(props) => px(props.lineNumber * LINE_HEIGHT)}; + height: ${LINE_HEIGHT}px; + top: ${(props) => props.lineNumber * LINE_HEIGHT}px; pointer-events: none; background-color: ${({ theme }) => tint(0.9, theme.eui.euiColorWarning)}; `; @@ -40,7 +39,7 @@ const LineNumberContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: absolute; top: 0; left: 0; - border-radius: ${borderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; background: ${({ isLibraryFrame, theme }) => isLibraryFrame ? theme.eui.euiColorEmptyShade @@ -49,29 +48,29 @@ const LineNumberContainer = euiStyled.div<{ isLibraryFrame: boolean }>` const LineNumber = euiStyled.div<{ highlight: boolean }>` position: relative; - min-width: ${px(units.eighth * 21)}; - padding-left: ${px(units.half)}; - padding-right: ${px(units.quarter)}; + min-width: 42px; + padding-left: ${({ theme }) => theme.eui.paddingSizes.s}; + padding-right: ${({ theme }) => theme.eui.paddingSizes.xs}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; - line-height: ${px(unit + units.eighth)}; + line-height: ${LINE_HEIGHT}px; text-align: right; border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; background-color: ${({ highlight, theme }) => highlight ? tint(0.9, theme.eui.euiColorWarning) : null}; &:last-of-type { - border-radius: 0 0 0 ${borderRadius}; + border-radius: 0 0 0 ${({ theme }) => theme.eui.euiBorderRadiusSmall}; } `; const LineContainer = euiStyled.div` overflow: auto; - margin: 0 0 0 ${px(units.eighth * 21)}; + margin: 0 0 0 42px; padding: 0; background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; &:last-of-type { - border-radius: 0 0 ${borderRadius} 0; + border-radius: 0 0 ${({ theme }) => theme.eui.euiBorderRadiusSmall} 0; } `; @@ -83,8 +82,8 @@ const Line = euiStyled.pre` border: 0; border-radius: 0; overflow: initial; - padding: 0 ${px(LINE_HEIGHT)}; - line-height: ${px(LINE_HEIGHT)}; + padding: 0 ${LINE_HEIGHT}px; + line-height: ${LINE_HEIGHT}px; `; const Code = euiStyled.code` diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index d361634759390c..0985153c2d39e2 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -12,22 +12,16 @@ import { Stackframe as StackframeType, StackframeWithLineContext, } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { - borderRadius, - fontFamilyCode, - fontSize, -} from '../../../style/variables'; import { Context } from './Context'; -import { FrameHeading } from './FrameHeading'; +import { FrameHeading } from './frame_heading'; import { Variables } from './Variables'; -import { px, units } from '../../../style/variables'; const ContextContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: relative; - font-family: ${fontFamilyCode}; - font-size: ${fontSize}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - border-radius: ${borderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; background: ${({ isLibraryFrame, theme }) => isLibraryFrame ? theme.eui.euiColorEmptyShade @@ -36,7 +30,7 @@ const ContextContainer = euiStyled.div<{ isLibraryFrame: boolean }>` // Indent the non-context frames the same amount as the accordion control const NoContextFrameHeadingWrapper = euiStyled.div` - margin-left: ${px(units.unit + units.half + units.quarter)}; + margin-left: 28px; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 7c090485937102..a43cd26e7f94a5 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -9,15 +9,16 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { borderRadius, px, unit, units } from '../../../style/variables'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; const VariablesContainer = euiStyled.div` background: ${({ theme }) => theme.eui.euiColorEmptyShade}; - border-radius: 0 0 ${borderRadius} ${borderRadius}; - padding: ${px(units.half)} ${px(unit)}; + border-radius: 0 0 ${({ theme }) => + `${theme.eui.euiBorderRadiusSmall} ${theme.eui.euiBorderRadiusSmall}`}; + padding: ${({ theme }) => + `${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.m}`}; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.test.tsx index a0ca8dd05b87ff..cc23452127fa4e 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React from 'react'; import { shallow } from 'enzyme'; -import { CauseStacktrace } from './CauseStacktrace'; +import React from 'react'; import { mountWithTheme } from '../../../utils/testHelpers'; +import { CauseStacktrace } from './cause_stacktrace'; describe('CauseStacktrace', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.tsx index 090ba0e8e28cf3..0ee4f66e2c24c0 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/cause_stacktrace.tsx @@ -5,17 +5,16 @@ * 2.0. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { px, unit, units } from '../../../style/variables'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; import { Stacktrace } from '.'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; const Accordion = euiStyled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; - margin-top: ${px(units.half)}; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; `; const CausedByContainer = euiStyled('h5')` @@ -31,7 +30,7 @@ const CausedByHeading = euiStyled('span')` `; const FramesContainer = euiStyled('div')` - padding-left: ${px(unit)}; + padding-left: ${({ theme }) => theme.eui.paddingSizes.m}; `; function CausedBy({ message }: { message: string }) { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.test.tsx similarity index 99% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.test.tsx index 794d88461358e4..2eb3320ef3aa31 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { renderWithTheme } from '../../../utils/testHelpers'; -import { FrameHeading } from './FrameHeading'; +import { FrameHeading } from './frame_heading'; function getRenderedStackframeText( stackframe: Stackframe, diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.tsx index 68b0893e1d8d33..8c9b9ae26c1404 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/frame_heading.tsx @@ -8,7 +8,6 @@ import React, { ComponentType } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; import { CSharpFrameHeadingRenderer, DefaultFrameHeadingRenderer, @@ -21,9 +20,9 @@ import { const FileDetails = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; line-height: 1.5; /* matches the line-hight of the accordion container button */ - padding: ${px(units.eighth)} 0; - font-family: ${fontFamilyCode}; - font-size: ${fontSize}; + padding: 2px 0; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; `; const LibraryFrameFileDetail = euiStyled.span` diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx index 19af5da30cff24..3395b22988e8cd 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx @@ -10,7 +10,7 @@ import { isEmpty, last } from 'lodash'; import React, { Fragment } from 'react'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { EmptyMessage } from '../../shared/EmptyMessage'; -import { LibraryStacktrace } from './LibraryStacktrace'; +import { LibraryStacktrace } from './library_stacktrace'; import { Stackframe as StackframeComponent } from './Stackframe'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.test.tsx similarity index 95% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.test.tsx index d583d4c7009395..d10e6c46cd12c7 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { renderWithTheme } from '../../../utils/testHelpers'; -import { LibraryStacktrace } from './LibraryStacktrace'; +import { LibraryStacktrace } from './library_stacktrace'; describe('LibraryStacktrace', () => { describe('render', () => { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.tsx index de417b465638fc..08399d09bed86d 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/library_stacktrace.tsx @@ -10,11 +10,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -import { px, units } from '../../../style/variables'; import { Stackframe as StackframeComponent } from './Stackframe'; const LibraryStacktraceAccordion = euiStyled(EuiAccordion)` - margin: ${px(units.quarter)} 0; + margin: ${({ theme }) => theme.eui.euiSizeXS} 0; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index 1ceccc5203fb2f..e0710556096c90 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { asDuration } from '../../../../common/utils/formatters'; -import { PercentOfParent } from '../../app/transaction_details/WaterfallWithSummmary/PercentOfParent'; +import { PercentOfParent } from '../../app/transaction_details/waterfall_with_summary/PercentOfParent'; interface Props { duration: number; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 8755003c89af2d..6939aaf49373e7 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -10,9 +10,9 @@ import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; -import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; +import { ErrorCountSummaryItemBadge } from './error_count_summary_item_badge'; import { isRumAgentName } from '../../../../common/agent_name'; -import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; +import { HttpInfoSummaryItem } from './http_info_summary_item'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; import { UserAgentSummaryItem } from './UserAgentSummaryItem'; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.test.tsx similarity index 90% rename from x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.test.tsx index 9996d1ea61a761..c6b77bddbf5449 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; +import { ErrorCountSummaryItemBadge } from './error_count_summary_item_badge'; import { expectTextsInDocument, renderWithTheme, diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.tsx index ec309f2f74d106..17189f1e40d943 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/error_count_summary_item_badge.tsx @@ -10,15 +10,13 @@ import { i18n } from '@kbn/i18n'; import { EuiBadge } from '@elastic/eui'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../hooks/use_theme'; -import { px } from '../../../../public/style/variables'; -import { units } from '../../../style/variables'; interface Props { count: number; } const Badge = euiStyled(EuiBadge)` - margin-top: ${px(units.eighth)}; + margin-top: 2px; `; export function ErrorCountSummaryItemBadge({ count }: Props) { diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/http_info_summary_item.test.tsx similarity index 69% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/http_info_summary_item.test.tsx index 71d7177051b9d7..3e1848af316fea 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/http_info_summary_item.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; import { HttpInfoSummaryItem } from '.'; import * as exampleTransactions from '../__fixtures__/transactions'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; describe('HttpInfoSummaryItem', () => { describe('render', () => { @@ -19,18 +20,24 @@ describe('HttpInfoSummaryItem', () => { it('renders', () => { expect(() => - shallow() + shallow(, { + wrappingComponent: EuiThemeProvider, + }) ).not.toThrowError(); }); it('renders empty component if no url is provided', () => { - const component = shallow(); + const component = shallow(, { + wrappingComponent: EuiThemeProvider, + }); expect(component.isEmptyRender()).toBeTruthy(); }); describe('with status code 100', () => { it('shows a success color', () => { - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(100); }); @@ -39,7 +46,9 @@ describe('HttpInfoSummaryItem', () => { describe('with status code 200', () => { it('shows a success color', () => { const p = { ...props, status: 200 }; - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(200); }); @@ -49,7 +58,9 @@ describe('HttpInfoSummaryItem', () => { it('shows a warning color', () => { const p = { ...props, status: 301 }; - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(301); }); @@ -59,7 +70,9 @@ describe('HttpInfoSummaryItem', () => { it('shows a error color', () => { const p = { ...props, status: 404 }; - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(404); }); @@ -69,7 +82,9 @@ describe('HttpInfoSummaryItem', () => { it('shows a error color', () => { const p = { ...props, status: 502 }; - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(502); }); @@ -79,7 +94,9 @@ describe('HttpInfoSummaryItem', () => { it('shows the default color', () => { const p = { ...props, status: 700 }; - const wrapper = mount(); + const wrapper = mount(, { + wrappingComponent: EuiThemeProvider, + }); expect(wrapper.find('HttpStatusBadge').prop('status')).toEqual(700); }); diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx similarity index 87% rename from x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx index d72f03c386226e..d10d15f8240a1c 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/http_info_summary_item/index.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import React from 'react'; -import { EuiToolTip, EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { units, px, truncate, unit } from '../../../../style/variables'; +import { truncate, unit } from '../../../../utils/style'; import { HttpStatusBadge } from '../HttpStatusBadge'; const HttpInfoBadge = euiStyled(EuiBadge)` - margin-right: ${px(units.quarter)}; + margin-right: ${({ theme }) => theme.eui.euiSizeXS}; `; const Url = euiStyled('span')` display: inline-block; vertical-align: bottom; - ${truncate(px(unit * 24))}; + ${truncate(unit * 24)}; `; interface HttpInfoProps { method?: string; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx index 395156800dceb5..1880ee00f3de73 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { px, units } from '../../../../public/style/variables'; import { Maybe } from '../../../../typings/common'; interface Props { @@ -18,7 +17,7 @@ interface Props { const Item = euiStyled(EuiFlexItem)` flex-wrap: nowrap; border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - padding-right: ${px(units.half)}; + padding-right: ${({ theme }) => theme.eui.paddingSizes.s}; flex-flow: row nowrap; line-height: 1.5; align-items: center !important; diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx index 28c000310346d5..b800ef41fbcac5 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx @@ -17,14 +17,14 @@ import { ENVIRONMENT_ALL, getEnvironmentLabel, } from '../../../../common/environment_filter_values'; -import { getAPMHref } from '../Links/apm/APMLink'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLicenseContext } from '../../../context/license/use_license_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { units } from '../../../style/variables'; +import { getAPMHref } from '../Links/apm/APMLink'; export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; @@ -39,6 +39,7 @@ export function AnomalyDetectionSetupLink() { const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const { basePath } = core.http; + const theme = useTheme(); return ( )} - + {ANOMALY_DETECTION_LINK_LABEL} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/AgentMarker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/agent_marker.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/AgentMarker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/agent_marker.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap index f108eb7ebf3ea4..5657732eb241bc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`Marker renders agent marker 1`] = ` @@ -25,7 +25,7 @@ exports[`Marker renders error marker 1`] = ` diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx similarity index 81% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx index 1411a264b065ef..0b7e405ae5d953 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AgentMarker } from './AgentMarker'; -import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { AgentMarker } from './agent_marker'; describe('AgentMarker', () => { const mark = { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx similarity index 80% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx index 3b7f0fab6c2a7c..947c7a93f38b14 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/agent_marker.tsx @@ -5,23 +5,22 @@ * 2.0. */ -import React from 'react'; import { EuiToolTip } from '@elastic/eui'; +import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; -import { px, units } from '../../../../../style/variables'; -import { Legend } from '../../Legend'; -import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { Legend } from '../legend'; const NameContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; - padding-bottom: ${px(units.half)}; + padding-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; const TimeContainer = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorMediumShade}; - padding-top: ${px(units.half)}; + padding-top: ${({ theme }) => theme.eui.paddingSizes.s}; `; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx index 36634f97a3a450..fdb97ea4fadde6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.test.tsx @@ -14,8 +14,8 @@ import { expectTextsInDocument, renderWithTheme, } from '../../../../../utils/testHelpers'; -import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; -import { ErrorMarker } from './ErrorMarker'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; +import { ErrorMarker } from './error_marker'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx similarity index 88% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx index 044070303d2ff1..b1e902957bfd7a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/error_marker.tsx @@ -5,36 +5,35 @@ * 2.0. */ -import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; +import React, { useState } from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { asDuration } from '../../../../../../common/utils/formatters'; -import { useTheme } from '../../../../../hooks/use_theme'; import { TRACE_ID, TRANSACTION_ID, } from '../../../../../../common/elasticsearch_fieldnames'; +import { asDuration } from '../../../../../../common/utils/formatters'; import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; -import { px, unit, units } from '../../../../../style/variables'; -import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { useTheme } from '../../../../../hooks/use_theme'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; -import { Legend, Shape } from '../../Legend'; +import { Legend, Shape } from '../legend'; interface Props { mark: ErrorMark; } const Popover = euiStyled.div` - max-width: ${px(280)}; + max-width: 280px; `; const TimeLegend = euiStyled(Legend)` - margin-bottom: ${px(unit)}; + margin-bottom: ${({ theme }) => theme.eui.euiSize}; `; const ErrorLink = euiStyled(ErrorDetailLink)` display: block; - margin: ${px(units.half)} 0 ${px(units.half)} 0; + margin: ${({ theme }) => `${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS} 0`}; overflow-wrap: break-word; `; @@ -102,7 +101,7 @@ export function ErrorMarker({ mark }: Props) { ( -
@
+
@
)} /> { it('renders agent marker', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index bece72b398d314..88318eb0e0c2d5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -7,11 +7,10 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { px } from '../../../../../style/variables'; -import { AgentMarker } from './AgentMarker'; -import { ErrorMarker } from './ErrorMarker'; -import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; +import { AgentMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { ErrorMark } from '../../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; +import { AgentMarker } from './agent_marker'; +import { ErrorMarker } from './error_marker'; interface Props { mark: ErrorMark | AgentMark; @@ -26,7 +25,7 @@ const MarkerContainer = euiStyled.div` export function Marker({ mark, x }: Props) { const legendWidth = 11; return ( - + {mark.type === 'errorMark' ? ( ) : ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx index 428da80fb808a4..2dcc969f661b87 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { VerticalGridLines, XYPlot } from 'react-vis'; import { useTheme } from '../../../../hooks/use_theme'; -import { Mark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks'; +import { Mark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks'; import { PlotValues } from './plotUtils'; interface VerticalLinesProps { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx index 650faa195271c5..6c7cb7a067d2e4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx @@ -8,11 +8,11 @@ import PropTypes from 'prop-types'; import React, { PureComponent, ReactNode } from 'react'; import { makeWidthFlexible } from 'react-vis'; +import { AgentMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_agent_marks'; +import { ErrorMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/Marks/get_error_marks'; import { getPlotValues } from './plotUtils'; -import { TimelineAxis } from './TimelineAxis'; +import { TimelineAxis } from './timeline_axis'; import { VerticalLines } from './VerticalLines'; -import { ErrorMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; -import { AgentMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; export type Mark = AgentMark | ErrorMark; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/legend.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/legend.tsx index f81da48b760e74..c7066565f0b22d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/legend.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../../hooks/use_theme'; -import { fontSizes, px, units } from '../../../../style/variables'; export enum Shape { circle = 'circle', @@ -17,7 +16,6 @@ export enum Shape { interface ContainerProps { onClick: (e: Event) => void; - fontSize?: string; clickable: boolean; disabled: boolean; } @@ -25,7 +23,7 @@ interface ContainerProps { const Container = euiStyled.div` display: flex; align-items: center; - font-size: ${(props) => props.fontSize}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; color: ${({ theme }) => theme.eui.euiColorDarkShade}; cursor: ${(props) => (props.clickable ? 'pointer' : 'initial')}; opacity: ${(props) => (props.disabled ? 0.4 : 1)}; @@ -33,16 +31,17 @@ const Container = euiStyled.div` `; interface IndicatorProps { - radius: number; color: string; shape: Shape; withMargin: boolean; } +const radius = 11; + export const Indicator = euiStyled.span` - width: ${(props) => px(props.radius)}; - height: ${(props) => px(props.radius)}; - margin-right: ${(props) => (props.withMargin ? px(props.radius / 2) : 0)}; + width: ${radius}px; + height: ${radius}px; + margin-right: ${(props) => (props.withMargin ? `${radius / 2}px` : 0)}; background: ${(props) => props.color}; border-radius: ${(props) => { return props.shape === Shape.circle ? '100%' : '0'; @@ -53,8 +52,6 @@ interface Props { onClick?: any; text?: string; color?: string; - fontSize?: string; - radius?: number; disabled?: boolean; clickable?: boolean; shape?: Shape; @@ -65,8 +62,6 @@ export function Legend({ onClick, text, color, - fontSize = fontSizes.small, - radius = units.minus - 1, disabled = false, clickable = false, shape = Shape.circle, @@ -81,18 +76,12 @@ export function Legend({ onClick={onClick} disabled={disabled} clickable={clickable || Boolean(onClick)} - fontSize={fontSize} {...rest} > {indicator ? ( indicator() ) : ( - + )} {text} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx similarity index 97% rename from x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx index 32b78bf8818a30..cf942ebb757763 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/timeline_axis.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import React, { ReactNode } from 'react'; import { inRange } from 'lodash'; +import React, { ReactNode } from 'react'; import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; -import { px } from '../../../../style/variables'; import { Mark } from './'; import { LastTickValue } from './LastTickValue'; import { Marker } from './Marker'; @@ -59,7 +58,7 @@ export function TimelineAxis({ position: 'sticky', top: 0, borderBottom: `1px solid ${theme.eui.euiColorMediumShade}`, - height: px(margins.top), + height: margins.top, zIndex: 2, width: '100%', }} diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 59205ef4985344..6b93fe9605e42b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import { EuiIcon } from '@elastic/eui'; import { AreaSeries, Chart, @@ -16,11 +13,13 @@ import { ScaleType, Settings, } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { merge } from 'lodash'; -import { Coordinate } from '../../../../../typings/timeseries'; +import React from 'react'; import { useChartTheme } from '../../../../../../observability/public'; -import { px, unit } from '../../../../style/variables'; +import { Coordinate } from '../../../../../typings/timeseries'; import { useTheme } from '../../../../hooks/use_theme'; +import { unit } from '../../../../utils/style'; import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; export type Color = @@ -73,8 +72,8 @@ export function SparkPlot({ const colorValue = theme.eui[color]; const chartSize = { - height: px(24), - width: compact ? px(unit * 3) : px(unit * 4), + height: theme.eui.euiSizeL, + width: compact ? unit * 3 : unit * 4, }; const Sparkline = hasComparisonSeries ? LineSeries : AreaSeries; diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 9e7a3ac744ffe6..9667bbd33cc736 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -40,7 +40,7 @@ import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; -import { unit } from '../../../style/variables'; +import { unit } from '../../../utils/style'; import { ChartContainer } from './chart_container'; import { onBrushEnd, isTimeseriesEmpty } from './helper/helper'; import { getLatencyChartSelector } from '../../../selectors/latency_chart_selectors'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 436eca47815023..a68373892e78b8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -34,7 +34,7 @@ import { useTheme } from '../../../../hooks/use_theme'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useAnnotationsContext } from '../../../../context/annotations/use_annotations_context'; import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context'; -import { unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { ChartContainer } from '../../charts/chart_container'; import { isTimeseriesEmpty, onBrushEnd } from '../../charts/helper/helper'; diff --git a/x-pack/plugins/apm/public/components/shared/key_value_filter_list/index.tsx b/x-pack/plugins/apm/public/components/shared/key_value_filter_list/index.tsx index 54d8790c32d33f..d28cc1646f16af 100644 --- a/x-pack/plugins/apm/public/components/shared/key_value_filter_list/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/key_value_filter_list/index.tsx @@ -19,7 +19,6 @@ import { import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { px, units } from '../../../style/variables'; interface KeyValue { key: string; @@ -34,7 +33,8 @@ const StyledEuiAccordion = styled(EuiAccordion)` `; const StyledEuiDescriptionList = styled(EuiDescriptionList)` - margin: ${px(units.half)} ${px(units.half)} 0 ${px(units.half)}; + margin: ${({ theme }) => + `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS}`}; .descriptionList__title, .descriptionList__description { border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/ClickOutside.js similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/ClickOutside.js diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestion.js similarity index 84% rename from x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestion.js index 987daf67b1fc7c..26b99c4e54f656 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestion.js @@ -9,13 +9,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { EuiIcon } from '@elastic/eui'; -import { - fontFamilyCode, - px, - units, - fontSizes, - unit, -} from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; import { tint } from 'polished'; function getIconColor(type, theme) { @@ -40,23 +34,23 @@ const Description = euiStyled.div` display: inline; span { - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; color: ${({ theme }) => theme.eui.euiColorFullShade}; - padding: 0 ${px(units.quarter)}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs}; display: inline-block; } } `; const ListItem = euiStyled.li` - font-size: ${fontSizes.small}; - height: ${px(units.double)}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + height: ${({ theme }) => theme.eui.euiSizeXL}; align-items: center; display: flex; background: ${({ selected, theme }) => selected ? theme.eui.euiColorLightestShade : 'initial'}; cursor: pointer; - border-radius: ${px(units.quarter)}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; ${Description} { p span { @@ -69,19 +63,19 @@ const ListItem = euiStyled.li` `; const Icon = euiStyled.div` - flex: 0 0 ${px(units.double)}; + flex: 0 0 ${({ theme }) => theme.eui.euiSizeXL}; background: ${({ type, theme }) => tint(0.9, getIconColor(type, theme))}; color: ${({ type, theme }) => getIconColor(type, theme)}; width: 100%; height: 100%; text-align: center; - line-height: ${px(units.double)}; + line-height: ${({ theme }) => theme.eui.euiSizeXL}; `; const TextValue = euiStyled.div` - flex: 0 0 ${px(unit * 16)}; + flex: 0 0 ${unit * 16}px; color: ${({ theme }) => theme.eui.euiColorDarkestShade}; - padding: 0 ${px(units.half)}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.s}; `; function getEuiIconType(type) { diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js similarity index 88% rename from x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js index 405be89c6629ca..386eb7e1e0d7d9 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/Suggestions.js @@ -5,25 +5,28 @@ * 2.0. */ -import React, { Component } from 'react'; +import { isEmpty } from 'lodash'; +import { tint } from 'polished'; import PropTypes from 'prop-types'; +import React, { Component } from 'react'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; -import { isEmpty } from 'lodash'; +import { unit } from '../../../../utils/style'; import Suggestion from './Suggestion'; -import { units, px, unit } from '../../../../style/variables'; -import { tint } from 'polished'; const List = euiStyled.ul` width: 100%; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - border-radius: ${px(units.quarter)}; - box-shadow: 0px ${px(units.quarter)} ${px(units.double)} - ${({ theme }) => tint(0.9, theme.eui.euiColorFullShade)}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadiusSmall}; + box-shadow: 0 ${({ theme }) => + `${theme.eui.euiSizeXS} ${theme.eui.euiSizeXL} ${tint( + 0.9, + theme.eui.euiColorFullShade + )}`}; position: absolute; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; z-index: 10; left: 0; - max-height: ${px(unit * 20)}; + max-height: ${unit * 20}px; overflow: scroll; `; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/index.js similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js rename to x-pack/plugins/apm/public/components/shared/kuery_bar/Typeahead/index.js diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts rename to x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx rename to x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts b/x-pack/plugins/apm/public/components/shared/kuery_bar/use_processor_event.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts rename to x-pack/plugins/apm/public/components/shared/kuery_bar/use_processor_event.ts diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/utils.ts b/x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/KueryBar/utils.ts rename to x-pack/plugins/apm/public/components/shared/kuery_bar/utils.ts diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap similarity index 90% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap rename to x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap index 655fc5a25b9ef5..55e0bc0fe24316 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/__snapshots__/ManagedTable.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ManagedTable component should render a page-full of items, with defaults 1`] = ` +exports[`ManagedTable should render a page-full of items, with defaults 1`] = ` `; -exports[`ManagedTable component should render when specifying initial values 1`] = ` +exports[`ManagedTable should render when specifying initial values 1`] = ` { field?: string; dataType?: string; align?: string; - width?: string; + width?: string | number; sortable?: boolean; render?: (value: any, item: T) => unknown; } diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/managed_table/managed_table.test.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js rename to x-pack/plugins/apm/public/components/shared/managed_table/managed_table.test.tsx index ab96eba14b403b..c474d98a3f0d4d 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/ManagedTable.test.js +++ b/x-pack/plugins/apm/public/components/shared/managed_table/managed_table.test.tsx @@ -7,39 +7,41 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { UnoptimizedManagedTable } from '.'; +import { ITableColumn, UnoptimizedManagedTable } from '.'; -describe('ManagedTable component', () => { - let people; - let columns; +interface Person { + name: string; + age: number; +} - beforeEach(() => { - people = [ - { name: 'Jess', age: 29 }, - { name: 'Becky', age: 43 }, - { name: 'Thomas', age: 31 }, - ]; - columns = [ - { - field: 'name', - name: 'Name', - sortable: true, - render: (name) => `Name: ${name}`, - }, - { field: 'age', name: 'Age', render: (age) => `Age: ${age}` }, - ]; - }); +describe('ManagedTable', () => { + const people: Person[] = [ + { name: 'Jess', age: 29 }, + { name: 'Becky', age: 43 }, + { name: 'Thomas', age: 31 }, + ]; + const columns: Array> = [ + { + field: 'name', + name: 'Name', + sortable: true, + render: (name) => `Name: ${name}`, + }, + { field: 'age', name: 'Age', render: (age) => `Age: ${age}` }, + ]; it('should render a page-full of items, with defaults', () => { expect( - shallow() + shallow( + columns={columns} items={people} /> + ) ).toMatchSnapshot(); }); it('should render when specifying initial values', () => { expect( shallow( - columns={columns} items={people} initialSortField="age" diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 74374e331ee8e4..4f9e58239855d8 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -19,9 +19,8 @@ import { enableInspectEsQueries } from '../../../../observability/public'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { useKibanaUrl } from '../../hooks/useKibanaUrl'; import { useBreakPoints } from '../../hooks/use_break_points'; -import { px } from '../../style/variables'; import { DatePicker } from './DatePicker'; -import { KueryBar } from './KueryBar'; +import { KueryBar } from './kuery_bar'; import { TimeComparison } from './time_comparison'; import { TransactionTypeSelect } from './transaction_type_select'; @@ -125,7 +124,7 @@ export function SearchBar({ responsive={false} > {showTimeComparison && ( - + )} diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx index 05305558564f13..695c941c61ed4b 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import React from 'react'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { px } from '../../../style/variables'; interface IconPopoverProps { title: string; @@ -59,7 +58,7 @@ export function IconPopover({ closePopover={onClose} > {title} -
+
{isLoading ? ( ) : ( diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/plugins/apm/public/components/shared/sticky_properties/__snapshots__/sticky_properties.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap rename to x-pack/plugins/apm/public/components/shared/sticky_properties/__snapshots__/sticky_properties.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/sticky_properties/index.tsx similarity index 93% rename from x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx rename to x-pack/plugins/apm/public/components/shared/sticky_properties/index.tsx index ee764db516d721..49488ba09e0683 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/sticky_properties/index.tsx @@ -9,13 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { - fontFamilyCode, - fontSizes, - px, - truncate, - units, -} from '../../../style/variables'; +import { truncate } from '../../../utils/style'; export interface IStickyProperty { val: JSX.Element | string | Date; @@ -26,12 +20,12 @@ export interface IStickyProperty { } const TooltipFieldName = euiStyled.span` - font-family: ${fontFamilyCode}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; `; const PropertyLabel = euiStyled.div` - margin-bottom: ${px(units.half)}; - font-size: ${fontSizes.small}; + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; span { diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js b/x-pack/plugins/apm/public/components/shared/sticky_properties/sticky_properties.test.tsx similarity index 80% rename from x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js rename to x-pack/plugins/apm/public/components/shared/sticky_properties/sticky_properties.test.tsx index eba70a171a1dfb..d29702752c159b 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js +++ b/x-pack/plugins/apm/public/components/shared/sticky_properties/sticky_properties.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { StickyProperties } from './index'; +import { StickyProperties } from '.'; import { shallow } from 'enzyme'; import { USER_ID, URL_FULL } from '../../../../common/elasticsearch_fieldnames'; import { mockMoment } from '../../../utils/testHelpers'; @@ -35,7 +35,7 @@ describe('StickyProperties', () => { { label: 'User ID', fieldName: USER_ID, - val: 1337, + val: '1337', }, ]; @@ -52,7 +52,7 @@ describe('StickyProperties', () => { { label: 'My Number', fieldName: 'myNumber', - val: 1337, + val: '1337', }, ]; @@ -66,25 +66,6 @@ describe('StickyProperties', () => { expect(wrapper).toEqual('1337'); }); - it('should not stringify booleans', () => { - const stickyProperties = [ - { - label: 'My boolean', - fieldName: 'myBoolean', - val: true, - }, - ]; - - const wrapper = shallow( - - ) - .find('PropertyValue') - .dive() - .text(); - - expect(wrapper).toEqual(''); - }); - it('should render nested components', () => { const stickyProperties = [ { diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index ed9d1a15cdbcaa..d7dfd3de2b6285 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -10,13 +10,12 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { useUiTracker } from '../../../../../observability/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useUiTracker } from '../../../../../observability/public'; import { getDateDifference } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { px, unit } from '../../../style/variables'; -import * as urlHelpers from '../../shared/Links/url_helpers'; import { useBreakPoints } from '../../../hooks/use_break_points'; +import * as urlHelpers from '../../shared/Links/url_helpers'; import { getTimeRangeComparison, TimeRangeComparisonType, @@ -28,7 +27,7 @@ const PrependContainer = euiStyled.div` align-items: center; background-color: ${({ theme }) => theme.eui.euiFormInputGroupLabelBackground}; - padding: 0 ${px(unit)}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.m}; `; function getDateFormat({ diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.tsx index b28b364adcf30f..a4f7c2b6634846 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/TransactionActionMenu.tsx @@ -22,7 +22,7 @@ import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLicenseContext } from '../../../context/license/use_license_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { CustomLinkMenuSection } from './CustomLinkMenuSection'; +import { CustomLinkMenuSection } from './custom_link_menu_section'; import { getSections } from './sections'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/__fixtures__/mockData.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__fixtures__/mockData.ts rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/__fixtures__/mockData.ts diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/TransactionActionMenu.test.tsx.snap similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/__snapshots__/TransactionActionMenu.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/CustomLinkToolbar.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.test.tsx index c0c4613e033c93..e063fb36b8e9c0 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.test.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import React from 'react'; import { render } from '@testing-library/react'; -import { CustomLinkList } from './CustomLinkList'; +import React from 'react'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../utils/testHelpers'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { CustomLinkList } from './custom_link_list'; describe('CustomLinkList', () => { const customLinks = [ diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.tsx similarity index 89% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.tsx index 02d6dd436040f8..7ea7cd35c443a6 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkList.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/custom_link_list.tsx @@ -8,12 +8,12 @@ import Mustache from 'mustache'; import React from 'react'; import { - SectionLinks, SectionLink, + SectionLinks, } from '../../../../../../observability/public'; import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { px, unit } from '../../../../style/variables'; +import { unit } from '../../../../utils/style'; export function CustomLinkList({ customLinks, @@ -23,7 +23,7 @@ export function CustomLinkList({ transaction: Transaction; }) { return ( - + {customLinks.map((link) => { const href = getHref(link, transaction); return ( diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx index cbbf34c78c4af6..bc234b88a08e4f 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/custom_link_menu_section/index.tsx @@ -5,37 +5,36 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; import { - EuiText, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiButtonEmpty, + EuiText, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { EuiToolTip } from '@elastic/eui'; -import { NO_PERMISSION_LABEL } from '../../../../../common/custom_link'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import React, { useMemo, useState } from 'react'; import { ActionMenuDivider, Section, SectionSubtitle, SectionTitle, } from '../../../../../../observability/public'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { CustomLinkList } from './CustomLinkList'; -import { CustomLinkToolbar } from './CustomLinkToolbar'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { LoadingStatePrompt } from '../../LoadingStatePrompt'; -import { px } from '../../../../style/variables'; -import { CreateEditCustomLinkFlyout } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout'; -import { convertFiltersToQuery } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/helper'; +import { NO_PERMISSION_LABEL } from '../../../../../common/custom_link'; import { CustomLink, Filter, } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { CreateEditCustomLinkFlyout } from '../../../app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout'; +import { convertFiltersToQuery } from '../../../app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/helper'; +import { LoadingStatePrompt } from '../../LoadingStatePrompt'; +import { CustomLinkToolbar } from './CustomLinkToolbar'; +import { CustomLinkList } from './custom_link_list'; const DEFAULT_LINKS_TO_SHOW = 3; @@ -165,7 +164,7 @@ function BottomSection({ return ( - + {i18n.translate('xpack.apm.customLink.empty', { defaultMessage: 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.', diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.test.ts rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts similarity index 93% rename from x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts rename to x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts index 0e30cfe3168f11..ebc48e1e9faf48 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts @@ -17,6 +17,7 @@ import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; import { getInfraHref } from '../Links/InfraLink'; import { fromQuery } from '../Links/url_helpers'; +import { SectionRecord, getNonEmptySections, Action } from './sections_helper'; function getInfraMetricsQuery(transaction: Transaction) { const timestamp = new Date(transaction['@timestamp']).getTime(); @@ -28,22 +29,6 @@ function getInfraMetricsQuery(transaction: Transaction) { }; } -interface Action { - key: string; - label: string; - href: string; - condition: boolean; -} - -interface Section { - key: string; - title?: string; - subtitle?: string; - actions: Action[]; -} - -type SectionRecord = Record; - export const getSections = ({ transaction, basePath, @@ -296,14 +281,5 @@ export const getSections = ({ }; // Filter out actions that shouldnt be shown and sections without any actions. - return Object.values(sectionRecord) - .map((sections) => - sections - .map((section) => ({ - ...section, - actions: section.actions.filter((action) => action.condition), - })) - .filter((section) => !isEmpty(section.actions)) - ) - .filter((sections) => !isEmpty(sections)); + return getNonEmptySections(sectionRecord); }; diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.test.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.test.ts new file mode 100644 index 00000000000000..741a66d71be14e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getNonEmptySections } from './sections_helper'; + +describe('getNonEmptySections', () => { + it('returns empty when no section is available', () => { + expect(getNonEmptySections({})).toEqual([]); + }); + it("returns empty when section doesn't have actions", () => { + expect( + getNonEmptySections({ + foo: [ + { + key: 'foo', + title: 'Foo', + subtitle: 'Foo bar', + actions: [], + }, + ], + }) + ).toEqual([]); + }); + + it('returns only sections with actions with condition true', () => { + expect( + getNonEmptySections({ + foo: [ + { + key: 'foo', + title: 'Foo', + subtitle: 'Foo bar', + actions: [], + }, + ], + bar: [ + { + key: 'bar', + title: 'Bar', + subtitle: 'Bar foo', + actions: [ + { + key: 'bar_action', + label: 'Bar Action', + condition: true, + }, + { + key: 'bar_action_2', + label: 'Bar Action 2', + condition: false, + }, + ], + }, + ], + }) + ).toEqual([ + [ + { + key: 'bar', + title: 'Bar', + subtitle: 'Bar foo', + actions: [ + { + key: 'bar_action', + label: 'Bar Action', + condition: true, + }, + ], + }, + ], + ]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.ts new file mode 100644 index 00000000000000..1632fdb6780131 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections_helper.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; + +export interface Action { + key: string; + label: string; + href?: string; + onClick?: () => void; + condition: boolean; +} + +interface Section { + key: string; + title?: string; + subtitle?: string; + actions: Action[]; +} + +export type SectionRecord = Record; + +/** Filter out actions that shouldnt be shown and sections without any actions. */ +export function getNonEmptySections(sectionRecord: SectionRecord) { + return Object.values(sectionRecord) + .map((sections) => + sections + .map((section) => ({ + ...section, + actions: section.actions.filter((action) => action.condition), + })) + .filter((section) => !isEmpty(section.actions)) + ) + .filter((sections) => !isEmpty(sections)); +} diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index 63e0b84362073d..d9268c14aa2ea3 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -8,7 +8,7 @@ import { EuiToolTip } from '@elastic/eui'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { truncate } from '../../../style/variables'; +import { truncate } from '../../../utils/style'; const tooltipAnchorClassname = '_apm_truncate_tooltip_anchor_'; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 46cbc26a74d2d1..0cd50095706138 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -22,6 +22,7 @@ import type { DataPublicPluginStart, } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { FleetStart } from '../../fleet/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import type { PluginSetupContract as AlertingPluginPublicSetup, @@ -43,6 +44,10 @@ import type { } from '../../triggers_actions_ui/public'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { featureCatalogueEntry } from './featureCatalogueEntry'; +import { + getApmEnrollmentFlyoutData, + LazyApmCustomAssetsExtension, +} from './components/fleet_integration'; export type ApmPluginSetup = ReturnType; @@ -69,6 +74,7 @@ export interface ApmPluginStartDeps { ml?: MlPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; + fleet: FleetStart; } export class ApmPlugin implements Plugin { @@ -303,5 +309,22 @@ export class ApmPlugin implements Plugin { return {}; } - public start(core: CoreStart, plugins: ApmPluginStartDeps) {} + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + const { fleet } = plugins; + + const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); + + fleet.registerExtension({ + package: 'apm', + view: 'agent-enrollment-flyout', + title: agentEnrollmentExtensionData.title, + Component: agentEnrollmentExtensionData.Component, + }); + + fleet.registerExtension({ + package: 'apm', + view: 'package-detail-assets', + Component: LazyApmCustomAssetsExtension, + }); + } } diff --git a/x-pack/plugins/apm/public/style/variables.ts b/x-pack/plugins/apm/public/style/variables.ts deleted file mode 100644 index 07ccf97c0dab06..00000000000000 --- a/x-pack/plugins/apm/public/style/variables.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Units -export const unit = 16; - -export const units = { - unit, - eighth: unit / 8, - quarter: unit / 4, - half: unit / 2, - minus: unit * 0.75, - plus: unit * 1.5, - double: unit * 2, - triple: unit * 3, - quadruple: unit * 4, -}; - -export function px(value: number): string { - return `${value}px`; -} - -export function pct(value: number): string { - return `${value}%`; -} - -// Styling -export const borderRadius = '4px'; - -// Fonts -export const fontFamilyCode = - '"Roboto Mono", Consolas, Menlo, Courier, monospace'; - -// Font sizes -export const fontSize = '14px'; - -export const fontSizes = { - tiny: '10px', - small: '12px', - large: '16px', - xlarge: '20px', - xxlarge: '30px', -}; - -export function truncate(width: string) { - return ` - max-width: ${width}; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - `; -} diff --git a/x-pack/plugins/apm/public/utils/style.ts b/x-pack/plugins/apm/public/utils/style.ts new file mode 100644 index 00000000000000..3c8aa5d339c6e9 --- /dev/null +++ b/x-pack/plugins/apm/public/utils/style.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const unit = 16; + +export function truncate(width: string | number) { + return ` + max-width: ${typeof width === 'string' ? width : `${width}px`}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + `; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts index 8bfb137c1689cf..60ce36a85235ed 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -6,7 +6,7 @@ */ import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; -import { rangeQuery } from '../../../../server/utils/queries'; +import { kqlQuery, rangeQuery } from '../../../../server/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TRANSACTION_DURATION, @@ -19,10 +19,12 @@ export async function getHasAggregatedTransactions({ start, end, apmEventClient, + kuery, }: { start?: number; end?: number; apmEventClient: APMEventClient; + kuery?: string; }) { const response = await apmEventClient.search( 'get_has_aggregated_transactions', @@ -36,6 +38,7 @@ export async function getHasAggregatedTransactions({ filter: [ { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, ...(start && end ? rangeQuery(start, end) : []), + ...kqlQuery(kuery), ], }, }, @@ -56,19 +59,22 @@ export async function getSearchAggregatedTransactions({ start, end, apmEventClient, + kuery, }: { config: APMConfig; start?: number; end?: number; apmEventClient: APMEventClient; + kuery?: string; }): Promise { const searchAggregatedTransactions = config['xpack.apm.searchAggregatedTransactions']; if ( + kuery || searchAggregatedTransactions === SearchAggregatedTransactionSetting.auto ) { - return getHasAggregatedTransactions({ start, end, apmEventClient }); + return getHasAggregatedTransactions({ start, end, apmEventClient, kuery }); } return ( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index 5820fd952c4491..7a511fc60fd064 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -9,7 +9,7 @@ import { shuffle, range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; import { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; -import { fetchTransactionDurationPecentiles } from './query_percentiles'; +import { fetchTransactionDurationPercentiles } from './query_percentiles'; import { fetchTransactionDurationCorrelation } from './query_correlation'; import { fetchTransactionDurationHistogramRangesteps } from './query_histogram_rangesteps'; import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; @@ -59,7 +59,7 @@ export const asyncSearchServiceProvider = ( const fetchCorrelations = async () => { try { // 95th percentile to be displayed as a marker in the log log chart - const percentileThreshold = await fetchTransactionDurationPecentiles( + const percentileThreshold = await fetchTransactionDurationPercentiles( esClient, params, params.percentileThreshold ? [params.percentileThreshold] : undefined @@ -93,7 +93,7 @@ export const asyncSearchServiceProvider = ( // Create an array of ranges [2, 4, 6, ..., 98] const percents = Array.from(range(2, 100, 2)); - const percentilesRecords = await fetchTransactionDurationPecentiles( + const percentilesRecords = await fetchTransactionDurationPercentiles( esClient, params, percents diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts new file mode 100644 index 00000000000000..12e897ab3eec92 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getQueryWithParams } from './get_query_with_params'; + +describe('correlations', () => { + describe('getQueryWithParams', () => { + it('returns the most basic query filtering on processor.event=transaction', () => { + const query = getQueryWithParams({ params: { index: 'apm-*' } }); + expect(query).toEqual({ + bool: { + filter: [{ term: { 'processor.event': 'transaction' } }], + }, + }); + }); + + it('returns a query considering additional params', () => { + const query = getQueryWithParams({ + params: { + index: 'apm-*', + serviceName: 'actualServiceName', + transactionName: 'actualTransactionName', + start: '01-01-2021', + end: '31-01-2021', + environment: 'dev', + percentileThresholdValue: 75, + }, + }); + expect(query).toEqual({ + bool: { + filter: [ + { term: { 'processor.event': 'transaction' } }, + { + term: { + 'service.name': 'actualServiceName', + }, + }, + { + term: { + 'transaction.name': 'actualTransactionName', + }, + }, + { + range: { + '@timestamp': { + gte: '01-01-2021', + lte: '31-01-2021', + }, + }, + }, + { + term: { + 'service.environment': 'dev', + }, + }, + { + range: { + 'transaction.duration.us': { + gte: 75, + }, + }, + }, + ], + }, + }); + }); + + it('returns a query considering a custom field/value pair', () => { + const query = getQueryWithParams({ + params: { index: 'apm-*' }, + fieldName: 'actualFieldName', + fieldValue: 'actualFieldValue', + }); + expect(query).toEqual({ + bool: { + filter: [ + { term: { 'processor.event': 'transaction' } }, + { + term: { + actualFieldName: 'actualFieldValue', + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts index e7cf8173b5bac6..08ba4b23fec35b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts @@ -43,6 +43,10 @@ const getRangeQuery = ( start?: string, end?: string ): estypes.QueryDslQueryContainer[] => { + if (start === undefined && end === undefined) { + return []; + } + return [ { range: { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts new file mode 100644 index 00000000000000..24741ebaa2daef --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationCorrelation, + getTransactionDurationCorrelationRequest, + BucketCorrelation, +} from './query_correlation'; + +const params = { index: 'apm-*' }; +const expectations = [1, 3, 5]; +const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; +const fractions = [1, 2, 4, 5]; +const totalDocCount = 1234; + +describe('query_correlation', () => { + describe('getTransactionDurationCorrelationRequest', () => { + it('applies options to the returned query with aggregations for correlations and k-test', () => { + const query = getTransactionDurationCorrelationRequest( + params, + expectations, + ranges, + fractions, + totalDocCount + ); + + expect(query.index).toBe(params.index); + + expect(query?.body?.aggs?.latency_ranges?.range?.field).toBe( + 'transaction.duration.us' + ); + expect(query?.body?.aggs?.latency_ranges?.range?.ranges).toEqual(ranges); + + expect( + (query?.body?.aggs?.transaction_duration_correlation as { + bucket_correlation: BucketCorrelation; + })?.bucket_correlation.function.count_correlation.indicator + ).toEqual({ + fractions, + expectations, + doc_count: totalDocCount, + }); + + expect( + (query?.body?.aggs?.ks_test as any)?.bucket_count_ks_test?.fractions + ).toEqual(fractions); + }); + }); + + describe('fetchTransactionDurationCorrelation', () => { + it('returns the data from the aggregations', async () => { + const latencyRangesBuckets = [{ to: 1 }, { from: 1, to: 2 }, { from: 2 }]; + const transactionDurationCorrelationValue = 0.45; + const KsTestLess = 0.01; + + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + latency_ranges: { + buckets: latencyRangesBuckets, + }, + transaction_duration_correlation: { + value: transactionDurationCorrelationValue, + }, + ks_test: { less: KsTestLess }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationCorrelation( + esClientMock, + params, + expectations, + ranges, + fractions, + totalDocCount + ); + + expect(resp).toEqual({ + correlation: transactionDurationCorrelationValue, + ksTest: KsTestLess, + ranges: latencyRangesBuckets, + }); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts index 9894ac54eccb66..f63c36f90d728a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts @@ -26,7 +26,7 @@ interface ResponseHit { _source: ResponseHitSource; } -interface BucketCorrelation { +export interface BucketCorrelation { buckets_path: string; function: { count_correlation: { @@ -80,8 +80,7 @@ export const getTransactionDurationCorrelationRequest = ( // KS test p value = ks_test.less ks_test: { bucket_count_ks_test: { - // Remove 0 after https://github.com/elastic/elasticsearch/pull/74624 is merged - fractions: [0, ...fractions], + fractions, buckets_path: 'latency_ranges>_count', alternative: ['less', 'greater', 'two_sided'], }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts new file mode 100644 index 00000000000000..89bdd4280d3249 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationFieldCandidates, + getRandomDocsRequest, + hasPrefixToInclude, + shouldBeExcluded, +} from './query_field_candidates'; + +const params = { index: 'apm-*' }; + +describe('query_field_candidates', () => { + describe('shouldBeExcluded', () => { + it('does not exclude a completely custom field name', () => { + expect(shouldBeExcluded('myFieldName')).toBe(false); + }); + + it(`excludes a field if it's one of FIELDS_TO_EXCLUDE_AS_CANDIDATE`, () => { + expect(shouldBeExcluded('transaction.type')).toBe(true); + }); + + it(`excludes a field if it's prefixed with one of FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE`, () => { + expect(shouldBeExcluded('observer.myFieldName')).toBe(true); + }); + }); + + describe('hasPrefixToInclude', () => { + it('identifies if a field name is prefixed to be included', () => { + expect(hasPrefixToInclude('myFieldName')).toBe(false); + expect(hasPrefixToInclude('somePrefix.myFieldName')).toBe(false); + expect(hasPrefixToInclude('cloud.myFieldName')).toBe(true); + expect(hasPrefixToInclude('labels.myFieldName')).toBe(true); + expect(hasPrefixToInclude('user_agent.myFieldName')).toBe(true); + }); + }); + + describe('getRandomDocsRequest', () => { + it('returns the most basic request body for a sample of random documents', () => { + const req = getRandomDocsRequest(params); + + expect(req).toEqual({ + body: { + _source: false, + fields: ['*'], + query: { + function_score: { + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + random_score: {}, + }, + }, + size: 1000, + }, + index: params.index, + }); + }); + }); + + describe('fetchTransactionDurationFieldCandidates', () => { + it('returns field candidates and total hits', async () => { + const esClientFieldCapsMock = jest.fn(() => ({ + body: { + fields: { + myIpFieldName: { ip: {} }, + myKeywordFieldName: { keyword: {} }, + myUnpopulatedKeywordFieldName: { keyword: {} }, + myNumericFieldName: { number: {} }, + }, + }, + })); + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + hits: { + hits: [ + { + fields: { + myIpFieldName: '1.1.1.1', + myKeywordFieldName: 'myKeywordFieldValue', + myNumericFieldName: 1234, + }, + }, + ], + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + fieldCaps: esClientFieldCapsMock, + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationFieldCandidates( + esClientMock, + params + ); + + expect(resp).toEqual({ + fieldCandidates: [ + // default field candidates + 'service.version', + 'service.node.name', + 'service.framework.version', + 'service.language.version', + 'service.runtime.version', + 'kubernetes.pod.name', + 'kubernetes.pod.uid', + 'container.id', + 'source.ip', + 'client.ip', + 'host.ip', + 'service.environment', + 'process.args', + 'http.response.status_code', + // field candidates identified by sample documents + 'myIpFieldName', + 'myKeywordFieldName', + ], + }); + expect(esClientFieldCapsMock).toHaveBeenCalledTimes(1); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts index 4f1840971da7d2..0fbdfef405e0d1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts @@ -21,7 +21,7 @@ import { POPULATED_DOC_COUNT_SAMPLE_SIZE, } from './constants'; -const shouldBeExcluded = (fieldName: string) => { +export const shouldBeExcluded = (fieldName: string) => { return ( FIELDS_TO_EXCLUDE_AS_CANDIDATE.has(fieldName) || FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE.some((prefix) => @@ -30,7 +30,7 @@ const shouldBeExcluded = (fieldName: string) => { ); }; -const hasPrefixToInclude = (fieldName: string) => { +export const hasPrefixToInclude = (fieldName: string) => { return FIELD_PREFIX_TO_ADD_AS_CANDIDATE.some((prefix) => fieldName.startsWith(prefix) ); @@ -50,8 +50,6 @@ export const getRandomDocsRequest = ( random_score: {}, }, }, - // Required value for later correlation queries - track_total_hits: true, size: POPULATED_DOC_COUNT_SAMPLE_SIZE, }, }); @@ -59,7 +57,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, params: SearchServiceParams -): Promise<{ fieldCandidates: Field[]; totalHits: number }> => { +): Promise<{ fieldCandidates: Field[] }> => { const { index } = params; // Get all fields with keyword mapping const respMapping = await esClient.fieldCaps({ @@ -100,6 +98,5 @@ export const fetchTransactionDurationFieldCandidates = async ( return { fieldCandidates: [...finalFieldCandidates], - totalHits: (resp.body.hits.total as estypes.SearchTotalHits).value, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts new file mode 100644 index 00000000000000..ea5a1f55bc9246 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { AsyncSearchProviderProgress } from '../../../../common/search_strategies/correlations/types'; + +import { + fetchTransactionDurationFieldValuePairs, + getTermsAggRequest, +} from './query_field_value_pairs'; + +const params = { index: 'apm-*' }; + +describe('query_field_value_pairs', () => { + describe('getTermsAggRequest', () => { + it('returns the most basic request body for a terms aggregation', () => { + const fieldName = 'myFieldName'; + const req = getTermsAggRequest(params, fieldName); + expect(req?.body?.aggs?.attribute_terms?.terms?.field).toBe(fieldName); + }); + }); + + describe('fetchTransactionDurationFieldValuePairs', () => { + it('returns field/value pairs for field candidates', async () => { + const fieldCandidates = [ + 'myFieldCandidate1', + 'myFieldCandidate2', + 'myFieldCandidate3', + ]; + const progress = { + loadedFieldValuePairs: 0, + } as AsyncSearchProviderProgress; + + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + attribute_terms: { + buckets: [{ key: 'myValue1' }, { key: 'myValue2' }], + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationFieldValuePairs( + esClientMock, + params, + fieldCandidates, + progress + ); + + expect(progress.loadedFieldValuePairs).toBe(1); + expect(resp).toEqual([ + { field: 'myFieldCandidate1', value: 'myValue1' }, + { field: 'myFieldCandidate1', value: 'myValue2' }, + { field: 'myFieldCandidate2', value: 'myValue1' }, + { field: 'myFieldCandidate2', value: 'myValue2' }, + { field: 'myFieldCandidate3', value: 'myValue1' }, + { field: 'myFieldCandidate3', value: 'myValue2' }, + ]); + expect(esClientSearchMock).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts index 703a203c892072..8fde9d3ab13783 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts @@ -52,7 +52,7 @@ export const fetchTransactionDurationFieldValuePairs = async ( ): Promise => { const fieldValuePairs: FieldValuePairs = []; - let fieldValuePairsProgress = 0; + let fieldValuePairsProgress = 1; for (let i = 0; i < fieldCandidates.length; i++) { const fieldName = fieldCandidates[i]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts new file mode 100644 index 00000000000000..6052841d277c3d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationFractions, + getTransactionDurationRangesRequest, +} from './query_fractions'; + +const params = { index: 'apm-*' }; +const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; + +describe('query_fractions', () => { + describe('getTransactionDurationRangesRequest', () => { + it('returns the request body for the transaction duration ranges aggregation', () => { + const req = getTransactionDurationRangesRequest(params, ranges); + + expect(req?.body?.aggs?.latency_ranges?.range?.field).toBe( + 'transaction.duration.us' + ); + expect(req?.body?.aggs?.latency_ranges?.range?.ranges).toEqual(ranges); + }); + }); + + describe('fetchTransactionDurationFractions', () => { + it('computes the actual percentile bucket counts and actual fractions', async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + latency_ranges: { + buckets: [{ doc_count: 1 }, { doc_count: 2 }], + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationFractions( + esClientMock, + params, + ranges + ); + + expect(resp).toEqual({ + fractions: [0.3333333333333333, 0.6666666666666666], + totalDocCount: 3, + }); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts new file mode 100644 index 00000000000000..2be94463522605 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationHistogram, + getTransactionDurationHistogramRequest, +} from './query_histogram'; + +const params = { index: 'apm-*' }; +const interval = 100; + +describe('query_histogram', () => { + describe('getTransactionDurationHistogramRequest', () => { + it('returns the request body for the histogram request', () => { + const req = getTransactionDurationHistogramRequest(params, interval); + + expect(req).toEqual({ + body: { + aggs: { + transaction_duration_histogram: { + histogram: { + field: 'transaction.duration.us', + interval, + }, + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + size: 0, + }, + index: 'apm-*', + }); + }); + }); + + describe('fetchTransactionDurationHistogram', () => { + it('returns the buckets from the histogram aggregation', async () => { + const histogramBucket = [ + { + key: 0.0, + doc_count: 1, + }, + ]; + + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + transaction_duration_histogram: { + buckets: histogramBucket, + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationHistogram( + esClientMock, + params, + interval + ); + + expect(resp).toEqual(histogramBucket); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts new file mode 100644 index 00000000000000..9ed529ccabddbe --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationHistogramInterval, + getHistogramIntervalRequest, +} from './query_histogram_interval'; + +const params = { index: 'apm-*' }; + +describe('query_histogram_interval', () => { + describe('getHistogramIntervalRequest', () => { + it('returns the request body for the transaction duration ranges aggregation', () => { + const req = getHistogramIntervalRequest(params); + + expect(req).toEqual({ + body: { + aggs: { + transaction_duration_max: { + max: { + field: 'transaction.duration.us', + }, + }, + transaction_duration_min: { + min: { + field: 'transaction.duration.us', + }, + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + size: 0, + }, + index: params.index, + }); + }); + }); + + describe('fetchTransactionDurationHistogramInterval', () => { + it('fetches the interval duration for histograms', async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + transaction_duration_max: { + value: 10000, + }, + transaction_duration_min: { + value: 10, + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationHistogramInterval( + esClientMock, + params + ); + + expect(resp).toEqual(10); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts new file mode 100644 index 00000000000000..bb366ea29fed48 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationHistogramRangesteps, + getHistogramIntervalRequest, +} from './query_histogram_rangesteps'; + +const params = { index: 'apm-*' }; + +describe('query_histogram_rangesteps', () => { + describe('getHistogramIntervalRequest', () => { + it('returns the request body for the histogram interval request', () => { + const req = getHistogramIntervalRequest(params); + + expect(req).toEqual({ + body: { + aggs: { + transaction_duration_max: { + max: { + field: 'transaction.duration.us', + }, + }, + transaction_duration_min: { + min: { + field: 'transaction.duration.us', + }, + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + size: 0, + }, + index: params.index, + }); + }); + }); + + describe('fetchTransactionDurationHistogramRangesteps', () => { + it('fetches the range steps for the log histogram', async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + transaction_duration_max: { + value: 10000, + }, + transaction_duration_min: { + value: 10, + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationHistogramRangesteps( + esClientMock, + params + ); + + expect(resp.length).toEqual(100); + expect(resp[0]).toEqual(9.260965422132594); + expect(resp[99]).toEqual(18521.930844265193); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts new file mode 100644 index 00000000000000..0c319aee0fb2b7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationPercentiles, + getTransactionDurationPercentilesRequest, +} from './query_percentiles'; + +const params = { index: 'apm-*' }; + +describe('query_percentiles', () => { + describe('getTransactionDurationPercentilesRequest', () => { + it('returns the request body for the duration percentiles request', () => { + const req = getTransactionDurationPercentilesRequest(params); + + expect(req).toEqual({ + body: { + aggs: { + transaction_duration_percentiles: { + percentiles: { + field: 'transaction.duration.us', + hdr: { + number_of_significant_value_digits: 3, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + size: 0, + }, + index: params.index, + }); + }); + }); + + describe('fetchTransactionDurationPercentiles', () => { + it('fetches the percentiles', async () => { + const percentilesValues = { + '1.0': 5.0, + '5.0': 25.0, + '25.0': 165.0, + '50.0': 445.0, + '75.0': 725.0, + '95.0': 945.0, + '99.0': 985.0, + }; + + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + transaction_duration_percentiles: { + values: percentilesValues, + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationPercentiles( + esClientMock, + params + ); + + expect(resp).toEqual(percentilesValues); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts index 013c1ba3cbc239..18dcefb59a11a5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts @@ -55,7 +55,7 @@ export const getTransactionDurationPercentilesRequest = ( }; }; -export const fetchTransactionDurationPecentiles = async ( +export const fetchTransactionDurationPercentiles = async ( esClient: ElasticsearchClient, params: SearchServiceParams, percents?: number[], @@ -73,7 +73,7 @@ export const fetchTransactionDurationPecentiles = async ( if (resp.body.aggregations === undefined) { throw new Error( - 'fetchTransactionDurationPecentiles failed, did not return aggregations.' + 'fetchTransactionDurationPercentiles failed, did not return aggregations.' ); } return ( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts new file mode 100644 index 00000000000000..9451928e47ded3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { + fetchTransactionDurationRanges, + getTransactionDurationRangesRequest, +} from './query_ranges'; + +const params = { index: 'apm-*' }; +const rangeSteps = [1, 3, 5]; + +describe('query_ranges', () => { + describe('getTransactionDurationRangesRequest', () => { + it('returns the request body for the duration percentiles request', () => { + const req = getTransactionDurationRangesRequest(params, rangeSteps); + + expect(req).toEqual({ + body: { + aggs: { + logspace_ranges: { + range: { + field: 'transaction.duration.us', + ranges: [ + { + to: 0, + }, + { + from: 0, + to: 1, + }, + { + from: 1, + to: 3, + }, + { + from: 3, + to: 5, + }, + { + from: 5, + }, + ], + }, + }, + }, + query: { + bool: { + filter: [ + { + term: { + 'processor.event': 'transaction', + }, + }, + ], + }, + }, + size: 0, + }, + index: params.index, + }); + }); + }); + + describe('fetchTransactionDurationRanges', () => { + it('fetches the percentiles', async () => { + const logspaceRangesBuckets = [ + { + key: '*-100.0', + to: 100.0, + doc_count: 2, + }, + { + key: '100.0-200.0', + from: 100.0, + to: 200.0, + doc_count: 2, + }, + { + key: '200.0-*', + from: 200.0, + doc_count: 3, + }, + ]; + + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + logspace_ranges: { + buckets: logspaceRangesBuckets, + }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const resp = await fetchTransactionDurationRanges( + esClientMock, + params, + rangeSteps + ); + + expect(resp).toEqual([ + { doc_count: 2, key: 100 }, + { doc_count: 3, key: 200 }, + ]); + expect(esClientSearchMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts index 88256f79150fc4..9074e7e0809bf7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts @@ -42,7 +42,9 @@ export const getTransactionDurationRangesRequest = ( }, [{ to: 0 }] as Array<{ from?: number; to?: number }> ); - ranges.push({ from: ranges[ranges.length - 1].to }); + if (ranges.length > 0) { + ranges.push({ from: ranges[ranges.length - 1].to }); + } return { index: params.index, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts new file mode 100644 index 00000000000000..6d4bfcdde99943 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import { SearchStrategyDependencies } from 'src/plugins/data/server'; + +import { + apmCorrelationsSearchStrategyProvider, + PartialSearchRequest, +} from './search_strategy'; + +// helper to trigger promises in the async search service +const flushPromises = () => new Promise(setImmediate); + +const clientFieldCapsMock = () => ({ body: { fields: [] } }); + +// minimal client mock to fulfill search requirements of the async search service to succeed +const clientSearchMock = ( + req: estypes.SearchRequest +): { body: estypes.SearchResponse } => { + let aggregations: + | { + transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; + } + | { + transaction_duration_min: estypes.AggregationsValueAggregate; + transaction_duration_max: estypes.AggregationsValueAggregate; + } + | { + logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ + from: number; + doc_count: number; + }>; + } + | { + latency_ranges: estypes.AggregationsMultiBucketAggregate<{ + doc_count: number; + }>; + } + | undefined; + + if (req?.body?.aggs !== undefined) { + const aggs = req.body.aggs; + // fetchTransactionDurationPercentiles + if (aggs.transaction_duration_percentiles !== undefined) { + aggregations = { transaction_duration_percentiles: { values: {} } }; + } + + // fetchTransactionDurationHistogramInterval + if ( + aggs.transaction_duration_min !== undefined && + aggs.transaction_duration_max !== undefined + ) { + aggregations = { + transaction_duration_min: { value: 0 }, + transaction_duration_max: { value: 1234 }, + }; + } + + // fetchTransactionDurationCorrelation + if (aggs.logspace_ranges !== undefined) { + aggregations = { logspace_ranges: { buckets: [] } }; + } + + // fetchTransactionDurationFractions + if (aggs.latency_ranges !== undefined) { + aggregations = { latency_ranges: { buckets: [] } }; + } + } + + return { + body: { + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + took: 162, + timed_out: false, + hits: { + hits: [], + total: { + value: 0, + relation: 'eq', + }, + }, + ...(aggregations !== undefined ? { aggregations } : {}), + }, + }; +}; + +describe('APM Correlations search strategy', () => { + describe('strategy interface', () => { + it('returns a custom search strategy with a `search` and `cancel` function', async () => { + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + expect(typeof searchStrategy.search).toBe('function'); + expect(typeof searchStrategy.cancel).toBe('function'); + }); + }); + + describe('search', () => { + let mockClientFieldCaps: jest.Mock; + let mockClientSearch: jest.Mock; + let mockDeps: SearchStrategyDependencies; + let params: Required['params']; + + beforeEach(() => { + mockClientFieldCaps = jest.fn(clientFieldCapsMock); + mockClientSearch = jest.fn(clientSearchMock); + mockDeps = ({ + esClient: { + asCurrentUser: { + fieldCaps: mockClientFieldCaps, + search: mockClientSearch, + }, + }, + } as unknown) as SearchStrategyDependencies; + params = { + index: 'apm-*', + }; + }); + + describe('async functionality', () => { + describe('when no params are provided', () => { + it('throws an error', async () => { + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + expect(() => searchStrategy.search({}, {}, mockDeps)).toThrow( + 'Invalid request parameters.' + ); + }); + }); + + describe('when no ID is provided', () => { + it('performs a client search with params', async () => { + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + await searchStrategy.search({ params }, {}, mockDeps).toPromise(); + const [[request]] = mockClientSearch.mock.calls; + + expect(request.index).toEqual('apm-*'); + expect(request.body).toEqual( + expect.objectContaining({ + aggs: { + transaction_duration_percentiles: { + percentiles: { + field: 'transaction.duration.us', + hdr: { number_of_significant_value_digits: 3 }, + }, + }, + }, + query: { + bool: { + filter: [{ term: { 'processor.event': 'transaction' } }], + }, + }, + size: 0, + }) + ); + }); + }); + + describe('when an ID with params is provided', () => { + it('retrieves the current request', async () => { + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const response = await searchStrategy + .search({ id: 'my-search-id', params }, {}, mockDeps) + .toPromise(); + + expect(response).toEqual( + expect.objectContaining({ id: 'my-search-id' }) + ); + }); + }); + + describe('if the client throws', () => { + it('does not emit an error', async () => { + mockClientSearch + .mockReset() + .mockRejectedValueOnce(new Error('client error')); + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const response = await searchStrategy + .search({ params }, {}, mockDeps) + .toPromise(); + + expect(response).toEqual( + expect.objectContaining({ isRunning: true }) + ); + }); + }); + + it('triggers the subscription only once', async () => { + expect.assertions(1); + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + searchStrategy + .search({ params }, {}, mockDeps) + .subscribe((response) => { + expect(response).toEqual( + expect.objectContaining({ loaded: 0, isRunning: true }) + ); + }); + }); + }); + + describe('response', () => { + it('sends an updated response on consecutive search calls', async () => { + const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + + const response1 = await searchStrategy + .search({ params }, {}, mockDeps) + .toPromise(); + + expect(typeof response1.id).toEqual('string'); + expect(response1).toEqual( + expect.objectContaining({ loaded: 0, isRunning: true }) + ); + + await flushPromises(); + + const response2 = await searchStrategy + .search({ id: response1.id, params }, {}, mockDeps) + .toPromise(); + + expect(response2.id).toEqual(response1.id); + expect(response2).toEqual( + expect.objectContaining({ loaded: 10, isRunning: false }) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts new file mode 100644 index 00000000000000..63de0a59d4894a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { computeExpectationsAndRanges } from './aggregation_utils'; + +describe('aggregation utils', () => { + describe('computeExpectationsAndRanges', () => { + it('returns expectations and ranges based on given percentiles #1', async () => { + const { expectations, ranges } = computeExpectationsAndRanges([0, 1]); + expect(expectations).toEqual([0, 0.5, 1]); + expect(ranges).toEqual([{ to: 0 }, { from: 0, to: 1 }, { from: 1 }]); + }); + it('returns expectations and ranges based on given percentiles #2', async () => { + const { expectations, ranges } = computeExpectationsAndRanges([1, 3, 5]); + expect(expectations).toEqual([1, 2, 4, 5]); + expect(ranges).toEqual([ + { to: 1 }, + { from: 1, to: 3 }, + { from: 3, to: 5 }, + { from: 5 }, + ]); + }); + it('returns expectations and ranges with adjusted fractions', async () => { + const { expectations, ranges } = computeExpectationsAndRanges([ + 1, + 3, + 3, + 5, + ]); + expect(expectations).toEqual([ + 1, + 2.333333333333333, + 3.666666666666667, + 5, + ]); + expect(ranges).toEqual([ + { to: 1 }, + { from: 1, to: 3 }, + { from: 3, to: 3 }, + { from: 3, to: 5 }, + { from: 5 }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts index 34e5ae2795d589..8d83b8fc29b058 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts @@ -31,14 +31,16 @@ export const computeExpectationsAndRanges = ( const ranges = percentiles.reduce((p, to) => { const from = p[p.length - 1]?.to; - if (from) { + if (from !== undefined) { p.push({ from, to }); } else { p.push({ to }); } return p; }, [] as Array<{ from?: number; to?: number }>); - ranges.push({ from: ranges[ranges.length - 1].to }); + if (ranges.length > 0) { + ranges.push({ from: ranges[ranges.length - 1].to }); + } const expectations = [tempPercentiles[0]]; for (let i = 1; i < tempPercentiles.length; i++) { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts new file mode 100644 index 00000000000000..ed4107b9d602a1 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRandomInt } from './math_utils'; + +describe('math utils', () => { + describe('getRandomInt', () => { + it('returns a random integer within the given range', () => { + const min = 0.9; + const max = 11.1; + const randomInt = getRandomInt(min, max); + expect(Number.isInteger(randomInt)).toBe(true); + expect(randomInt > min).toBe(true); + expect(randomInt < max).toBe(true); + }); + + it('returns 1 if given range only allows this integer', () => { + const randomInt = getRandomInt(0.9, 1.1); + expect(randomInt).toBe(1); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 4384d2be78ca04..3329119726bb56 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -51,9 +51,10 @@ const servicesRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { params, logger } = resources; const { environment, kuery } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getServices({ environment, @@ -405,9 +406,10 @@ const serviceThroughputRoute = createApmServerRoute({ comparisonStart, comparisonEnd, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); const { start, end } = setup; @@ -477,9 +479,10 @@ const serviceInstancesMainStatisticsRoute = createApmServerRoute({ comparisonEnd, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); const { start, end } = setup; @@ -552,9 +555,10 @@ const serviceInstancesDetailedStatisticsRoute = createApmServerRoute({ latencyAggregationType, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getServiceInstancesDetailedStatisticsPeriods({ environment, @@ -593,9 +597,10 @@ export const serviceInstancesMetadataDetails = createApmServerRoute({ const { serviceName, serviceNodeName } = resources.params.path; const { transactionType, environment, kuery } = resources.params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return await getServiceInstanceMetadataDetails({ searchAggregatedTransactions, diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 7fce04644f2205..bed7252dd20fda 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -26,9 +26,10 @@ const tracesRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { params } = resources; const { environment, kuery } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getTransactionGroupList( { environment, kuery, type: 'top_traces', searchAggregatedTransactions }, diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index bcc554e552fc33..c20de31847e8a3 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -56,9 +56,10 @@ const transactionGroupsRoute = createApmServerRoute({ const { serviceName } = params.path; const { environment, kuery, transactionType } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getTransactionGroupList( { @@ -95,16 +96,16 @@ const transactionGroupsMainStatisticsRoute = createApmServerRoute({ handler: async (resources) => { const { params } = resources; const setup = await setupRequest(resources); - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - const { path: { serviceName }, query: { environment, kuery, latencyAggregationType, transactionType }, } = params; + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); + return getServiceTransactionGroups({ environment, kuery, @@ -140,11 +141,6 @@ const transactionGroupsDetailedStatisticsRoute = createApmServerRoute({ }, handler: async (resources) => { const setup = await setupRequest(resources); - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - const { params } = resources; const { @@ -161,6 +157,11 @@ const transactionGroupsDetailedStatisticsRoute = createApmServerRoute({ }, } = params; + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); + return await getServiceTransactionGroupDetailedStatisticsPeriods({ environment, kuery, @@ -208,9 +209,10 @@ const transactionLatencyChartsRoute = createApmServerRoute({ comparisonEnd, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); const options = { environment, @@ -276,9 +278,10 @@ const transactionThroughputChartsRoute = createApmServerRoute({ transactionName, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return await getThroughputCharts({ environment, @@ -327,9 +330,10 @@ const transactionChartsDistributionRoute = createApmServerRoute({ traceId = '', } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getTransactionDistribution({ environment, @@ -411,9 +415,10 @@ const transactionChartsErrorRateRoute = createApmServerRoute({ comparisonEnd, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + ...setup, + kuery, + }); return getErrorRatePeriods({ environment, diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index 716f757b7c25e9..dd7df7059a7a44 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -13,7 +13,7 @@ import { SavedObjectFinderUi, SavedObjectMetaData, } from '../../../../../../src/plugins/saved_objects/public/'; -import { useServices } from '../../services'; +import { usePlatformService, useServices } from '../../services'; const strings = { getNoItemsText: () => @@ -33,9 +33,10 @@ export interface Props { export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { const services = useServices(); - const { embeddables, platform } = services; + const platformService = usePlatformService(); + const { embeddables } = services; const { getEmbeddableFactories } = embeddables; - const { getSavedObjects, getUISettings } = platform; + const { getSavedObjects, getUISettings } = platformService; const onAddPanel = (id: string, savedObjectType: string, name: string) => { const embeddableFactories = getEmbeddableFactories(); diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index dca549b6b38edb..1c95e844997a03 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -97,7 +97,7 @@ export const Toolbar: FC = ({ {workpadName} - + ( workpad: { id: 'coolworkpad', name: 'Workpad of Cool', height: 10, width: 7 }, pageCount: 11, }} - sharingServices={{ basePath: platformService.getBasePathInterface() }} + sharingServices={{}} onExport={action('onExport')} /> )); @@ -30,7 +29,6 @@ storiesOf('components/WorkpadHeader/ShareMenu', module).add('with Reporting', () pageCount: 11, }} sharingServices={{ - basePath: platformService.getBasePathInterface(), reporting: reportingService.start, }} onExport={action('onExport')} diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx index 4d38cc9fb88b41..d0c1eb550ea604 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/__stories__/flyout.stories.tsx @@ -26,17 +26,12 @@ storiesOf('components/WorkpadHeader/ShareMenu/ShareWebsiteFlyout', module) }, }) .add('default', () => ( - + )) .add('unsupported renderers', () => ( )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx index 5da009e050a27d..be337a6dcf00c8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx @@ -24,13 +24,42 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { arrayBufferFetch } from '../../../../../common/lib/fetch'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; +import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; +import { + downloadRenderedWorkpad, + downloadRuntime, + downloadZippedRuntime, +} from '../../../../lib/download_workpad'; import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants'; import { OnCloseFn } from '../share_menu.component'; import { WorkpadStep } from './workpad_step'; import { RuntimeStep } from './runtime_step'; import { SnippetsStep } from './snippets_step'; +import { useNotifyService, usePlatformService } from '../../../../services'; const strings = { + getCopyShareConfigMessage: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { + defaultMessage: 'Copied share markup to clipboard', + }), + getShareableZipErrorTitle: (workpadName: string) => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { + defaultMessage: + "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", + values: { + ZIP, + workpadName, + }, + }), + getUnknownExportErrorMessage: (type: string) => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { + defaultMessage: 'Unknown export type: {type}', + values: { + type, + }, + }), getRuntimeStepTitle: () => i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { defaultMessage: 'Download runtime', @@ -66,10 +95,9 @@ export type OnDownloadFn = (type: 'share' | 'shareRuntime' | 'shareZip') => void export type OnCopyFn = () => void; export interface Props { - onCopy: OnCopyFn; - onDownload: OnDownloadFn; onClose: OnCloseFn; unsupportedRenderers?: string[]; + renderedWorkpad: CanvasRenderedWorkpad; } const steps = (onDownload: OnDownloadFn, onCopy: OnCopyFn) => [ @@ -88,11 +116,39 @@ const steps = (onDownload: OnDownloadFn, onCopy: OnCopyFn) => [ ]; export const ShareWebsiteFlyout: FC = ({ - onCopy, - onDownload, onClose, unsupportedRenderers, + renderedWorkpad, }) => { + const notifyService = useNotifyService(); + const platformService = usePlatformService(); + const onCopy = () => { + notifyService.info(strings.getCopyShareConfigMessage()); + }; + + const onDownload = (type: 'share' | 'shareRuntime' | 'shareZip') => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(platformService.getBasePath()); + case 'shareZip': + const basePath = platformService.getBasePath(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then((blob) => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + notifyService.error(err, { + title: strings.getShareableZipErrorTitle(renderedWorkpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }; + const link = ( - i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { - defaultMessage: 'Copied share markup to clipboard', - }), - getShareableZipErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { - defaultMessage: - "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", - values: { - ZIP, - workpadName, - }, - }), - getUnknownExportErrorMessage: (type: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { - defaultMessage: 'Unknown export type: {type}', - values: { - type, - }, - }), -}; - const getUnsupportedRenderers = (state: State) => { const renderers: string[] = []; const expressions = getRenderedWorkpadExpressions(state); @@ -86,41 +52,10 @@ export const ShareWebsiteFlyout = compose connect(mapStateToProps), withKibana, withProps( - ({ - unsupportedRenderers, + ({ unsupportedRenderers, renderedWorkpad, onClose, workpad }: Props): ComponentProps => ({ renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ unsupportedRenderers, onClose, - onCopy: () => { - pluginServices.getServices().notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: (type) => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then((blob) => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - pluginServices.getServices().notify.error(err, { - title: strings.getShareableZipErrorTitle(workpad.name), - }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, }) ) )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx index 5ccc09bf3586b3..8d150d3d369722 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx @@ -9,11 +9,11 @@ import React, { FunctionComponent, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IBasePath } from 'kibana/public'; import { ReportingStart } from '../../../../../reporting/public'; import { PDF, JSON } from '../../../../i18n/constants'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { usePlatformService } from '../../../services'; import { ClosePopoverFn, Popover } from '../../popover'; import { ShareWebsiteFlyout } from './flyout'; import { CanvasWorkpadSharingData, getPdfJobParams } from './utils'; @@ -59,8 +59,6 @@ export interface Props { /** Canvas workpad to export as PDF **/ sharingData: CanvasWorkpadSharingData; sharingServices: { - /** BasePath dependency **/ - basePath: IBasePath; /** Reporting dependency **/ reporting?: ReportingStart; }; @@ -76,6 +74,7 @@ export const ShareMenu: FunctionComponent = ({ sharingServices: services, onExport, }) => { + const platformService = usePlatformService(); const [showFlyout, setShowFlyout] = useState(false); const onClose = () => { @@ -102,7 +101,9 @@ export const ShareMenu: FunctionComponent = ({ title: strings.getShareDownloadPDFTitle(), content: ( getPdfJobParams(sharingData, services.basePath)} + getJobParams={() => + getPdfJobParams(sharingData, platformService.getBasePathInterface()) + } layoutOption="canvas" onClose={closePopover} /> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts index ef13655b66aca9..f514f813599b68 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -41,12 +41,11 @@ export const ShareMenu = compose( withProps( ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => { const { - platform, reporting: { start: reporting }, } = services; return { - sharingServices: { basePath: platform.getBasePathInterface(), reporting }, + sharingServices: { reporting }, sharingData: { workpad, pageCount }, onExport: (type) => { switch (type) { diff --git a/x-pack/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/plugins/canvas/public/lib/custom_element_service.ts index aa3229456ebf65..6da624bb5d3ae0 100644 --- a/x-pack/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/plugins/canvas/public/lib/custom_element_service.ts @@ -9,10 +9,10 @@ import { AxiosPromise } from 'axios'; import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { CustomElement } from '../../types'; -import { platformService } from '../services'; +import { pluginServices } from '../services'; const getApiPath = function () { - const basePath = platformService.getService().getBasePath(); + const basePath = pluginServices.getServices().platform.getBasePath(); return `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; }; diff --git a/x-pack/plugins/canvas/public/lib/es_service.ts b/x-pack/plugins/canvas/public/lib/es_service.ts index c1a4a17970ffa7..0ff6c9e5d110f5 100644 --- a/x-pack/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/plugins/canvas/public/lib/es_service.ts @@ -6,28 +6,29 @@ */ // TODO - clint: convert to service abstraction - import { IndexPatternAttributes } from 'src/plugins/data/public'; import { API_ROUTE } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; import { pluginServices } from '../services'; -import { platformService } from '../services'; const { esService: strings } = ErrorStrings; const getApiPath = function () { - const basePath = platformService.getService().getBasePath(); + const platformService = pluginServices.getServices().platform; + const basePath = platformService.getBasePath(); return basePath + API_ROUTE; }; const getSavedObjectsClient = function () { - return platformService.getService().getSavedObjectsClient(); + const platformService = pluginServices.getServices().platform; + return platformService.getSavedObjectsClient(); }; const getAdvancedSettings = function () { - return platformService.getService().getUISettings(); + const platformService = pluginServices.getServices().platform; + return platformService.getUISettings(); }; export const getFields = (index = '_all') => { diff --git a/x-pack/plugins/canvas/public/lib/fullscreen.js b/x-pack/plugins/canvas/public/lib/fullscreen.js index f3f6e029696ead..fd4e0b65785b98 100644 --- a/x-pack/plugins/canvas/public/lib/fullscreen.js +++ b/x-pack/plugins/canvas/public/lib/fullscreen.js @@ -5,21 +5,22 @@ * 2.0. */ -import { platformService } from '../services'; +import { pluginServices } from '../services'; export const fullscreenClass = 'canvas-isFullscreen'; export function setFullscreen(fullscreen, doc = document) { + const platformService = pluginServices.getServices().platform; const enabled = Boolean(fullscreen); const body = doc.querySelector('body'); const bodyClassList = body.classList; const isFullscreen = bodyClassList.contains(fullscreenClass); if (enabled && !isFullscreen) { - platformService.getService().setFullscreen(false); + platformService.setFullscreen(false); bodyClassList.add(fullscreenClass); } else if (!enabled && isFullscreen) { bodyClassList.remove(fullscreenClass); - platformService.getService().setFullscreen(true); + platformService.setFullscreen(true); } } diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/plugins/canvas/public/lib/run_interpreter.ts index 149e90a8f6b730..d69f89566cfc9f 100644 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.ts +++ b/x-pack/plugins/canvas/public/lib/run_interpreter.ts @@ -6,6 +6,7 @@ */ import { fromExpression, getType } from '@kbn/interpreter/common'; +import { pluck } from 'rxjs/operators'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; import { pluginServices, expressionsService } from '../services'; @@ -21,7 +22,12 @@ export async function interpretAst( variables: Record ): Promise { const context = { variables }; - return await expressionsService.getService().execute(ast, null, context).getData(); + return await expressionsService + .getService() + .execute(ast, null, context) + .getData() + .pipe(pluck('result')) + .toPromise(); } /** @@ -43,7 +49,12 @@ export async function runInterpreter( const context = { variables }; try { - const renderable = await expressionsService.getService().execute(ast, input, context).getData(); + const renderable = await expressionsService + .getService() + .execute(ast, input, context) + .getData() + .pipe(pluck('result')) + .toPromise(); if (getType(renderable) === 'render') { return renderable; diff --git a/x-pack/plugins/canvas/public/lib/template_service.ts b/x-pack/plugins/canvas/public/lib/template_service.ts index 6a262d57672e45..d5ec467f18740e 100644 --- a/x-pack/plugins/canvas/public/lib/template_service.ts +++ b/x-pack/plugins/canvas/public/lib/template_service.ts @@ -5,13 +5,16 @@ * 2.0. */ +// TODO - clint: convert to service abstraction + import { API_ROUTE_TEMPLATES } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { platformService } from '../services'; +import { pluginServices } from '../services'; import { CanvasTemplate } from '../../types'; const getApiPath = function () { - const basePath = platformService.getService().getBasePath(); + const platformService = pluginServices.getServices().platform; + const basePath = platformService.getBasePath(); return `${basePath}${API_ROUTE_TEMPLATES}`; }; @@ -21,6 +24,5 @@ interface ListResponse { export async function list() { const templateResponse = await fetch.get(`${getApiPath()}`); - return templateResponse.data.templates; } diff --git a/x-pack/plugins/canvas/public/lib/workpad_service.js b/x-pack/plugins/canvas/public/lib/workpad_service.js index 70aa1c3f1f816e..20ad82860f1fa7 100644 --- a/x-pack/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/plugins/canvas/public/lib/workpad_service.js @@ -5,6 +5,7 @@ * 2.0. */ +// TODO: clint - move to workpad service. import { API_ROUTE_WORKPAD, API_ROUTE_WORKPAD_ASSETS, @@ -12,7 +13,7 @@ import { DEFAULT_WORKPAD_CSS, } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { platformService } from '../services'; +import { pluginServices } from '../services'; /* Remove any top level keys from the workpad which will be rejected by validation @@ -46,17 +47,20 @@ const sanitizeWorkpad = function (workpad) { }; const getApiPath = function () { - const basePath = platformService.getService().getBasePath(); + const platformService = pluginServices.getServices().platform; + const basePath = platformService.getBasePath(); return `${basePath}${API_ROUTE_WORKPAD}`; }; const getApiPathStructures = function () { - const basePath = platformService.getService().getBasePath(); + const platformService = pluginServices.getServices().platform; + const basePath = platformService.getBasePath(); return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`; }; const getApiPathAssets = function () { - const basePath = platformService.getService().getBasePath(); + const platformService = pluginServices.getServices().platform; + const basePath = platformService.getBasePath(); return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`; }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts index ab26625038bc58..9021c6d6c27533 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts @@ -5,15 +5,14 @@ * 2.0. */ import { useContext, useEffect } from 'react'; -import { useServices } from '../../../services'; +import { usePlatformService } from '../../../services'; import { WorkpadRoutingContext } from '..'; const fullscreenClass = 'canvas-isFullscreen'; export const useFullscreenPresentationHelper = () => { const { isFullscreen } = useContext(WorkpadRoutingContext); - const services = useServices(); - const { setFullscreen } = services.platform; + const { setFullscreen } = usePlatformService(); useEffect(() => { const body = document.querySelector('body'); diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index ccb38cd1a1e0f2..bdf84de7a47bd2 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -14,21 +14,21 @@ import { getWorkpad } from '../../state/selectors/workpad'; import { useFullscreenPresentationHelper } from './hooks/use_fullscreen_presentation_helper'; import { useAutoplayHelper } from './hooks/use_autoplay_helper'; import { useRefreshHelper } from './hooks/use_refresh_helper'; -import { useServices } from '../../services'; +import { usePlatformService } from '../../services'; export const WorkpadPresentationHelper: FC = ({ children }) => { - const services = useServices(); + const platformService = usePlatformService(); const workpad = useSelector(getWorkpad); useFullscreenPresentationHelper(); useAutoplayHelper(); useRefreshHelper(); useEffect(() => { - services.platform.setBreadcrumbs([ + platformService.setBreadcrumbs([ getBaseBreadcrumb(), getWorkpadBreadcrumb({ name: workpad.name }), ]); - }, [workpad.name, workpad.id, services.platform]); + }, [workpad.name, workpad.id, platformService]); useEffect(() => { setDocTitle(workpad.name); diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 83a54a8a673a1b..0aaef4ef280f0a 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -10,13 +10,16 @@ export * from './legacy'; import { PluginServices } from '../../../../../src/plugins/presentation_util/public'; import { CanvasWorkpadService } from './workpad'; import { CanvasNotifyService } from './notify'; +import { CanvasPlatformService } from './platform'; export interface CanvasPluginServices { workpad: CanvasWorkpadService; notify: CanvasNotifyService; + platform: CanvasPlatformService; } export const pluginServices = new PluginServices(); export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); +export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 7bb2be3f77e27e..bb0095a3c525ce 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -14,11 +14,13 @@ import { import { workpadServiceFactory } from './workpad'; import { notifyServiceFactory } from './notify'; +import { platformServiceFactory } from './platform'; import { CanvasPluginServices } from '..'; import { CanvasStartDeps } from '../../plugin'; export { workpadServiceFactory } from './workpad'; export { notifyServiceFactory } from './notify'; +export { platformServiceFactory } from './platform'; export const pluginServiceProviders: PluginServiceProviders< CanvasPluginServices, @@ -26,6 +28,7 @@ export const pluginServiceProviders: PluginServiceProviders< > = { workpad: new PluginServiceProvider(workpadServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), + platform: new PluginServiceProvider(platformServiceFactory), }; export const pluginServiceRegistry = new PluginServiceRegistry< diff --git a/x-pack/plugins/canvas/public/services/legacy/platform.ts b/x-pack/plugins/canvas/public/services/kibana/platform.ts similarity index 54% rename from x-pack/plugins/canvas/public/services/legacy/platform.ts rename to x-pack/plugins/canvas/public/services/kibana/platform.ts index b867622f5d3023..79eae8d8081bba 100644 --- a/x-pack/plugins/canvas/public/services/legacy/platform.ts +++ b/x-pack/plugins/canvas/public/services/kibana/platform.ts @@ -5,38 +5,17 @@ * 2.0. */ -import { - SavedObjectsStart, - SavedObjectsClientContract, - IUiSettingsClient, - ChromeBreadcrumb, - IBasePath, - ChromeStart, -} from '../../../../../../src/core/public'; -import { CanvasServiceFactory } from '.'; +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; -export interface PlatformService { - getBasePath: () => string; - getBasePathInterface: () => IBasePath; - getDocLinkVersion: () => string; - getElasticWebsiteUrl: () => string; - getHasWriteAccess: () => boolean; - getUISetting: (key: string, defaultValue?: any) => any; - setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; - setRecentlyAccessed: (link: string, label: string, id: string) => void; - setFullscreen: ChromeStart['setIsVisible']; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasPlatformService } from '../platform'; - // TODO: these should go away. We want thin accessors, not entire objects. - // Entire objects are hard to mock, and hide our dependency on the external service. - getSavedObjects: () => SavedObjectsStart; - getSavedObjectsClient: () => SavedObjectsClientContract; - getUISettings: () => IUiSettingsClient; -} +export type CanvaPlatformServiceFactory = KibanaPluginServiceFactory< + CanvasPlatformService, + CanvasStartDeps +>; -export const platformServiceFactory: CanvasServiceFactory = ( - _coreSetup, - coreStart -) => { +export const platformServiceFactory: CanvaPlatformServiceFactory = ({ coreStart }) => { return { getBasePath: coreStart.http.basePath.get, getBasePathInterface: () => coreStart.http.basePath, diff --git a/x-pack/plugins/canvas/public/services/legacy/context.tsx b/x-pack/plugins/canvas/public/services/legacy/context.tsx index dd2e45740f041b..2f472afd7d3c1b 100644 --- a/x-pack/plugins/canvas/public/services/legacy/context.tsx +++ b/x-pack/plugins/canvas/public/services/legacy/context.tsx @@ -22,7 +22,6 @@ export interface WithServicesProps { const defaultContextValue = { embeddables: {}, expressions: {}, - platform: {}, navLink: {}, search: {}, }; @@ -30,7 +29,6 @@ const defaultContextValue = { const context = createContext(defaultContextValue as CanvasServices); export const useServices = () => useContext(context); -export const usePlatformService = () => useServices().platform; export const useEmbeddablesService = () => useServices().embeddables; export const useExpressionsService = () => useServices().expressions; export const useNavLinkService = () => useServices().navLink; @@ -50,7 +48,6 @@ export const LegacyServicesProvider: FC<{ const value = { embeddables: specifiedProviders.embeddables.getService(), expressions: specifiedProviders.expressions.getService(), - platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), search: specifiedProviders.search.getService(), reporting: specifiedProviders.reporting.getService(), diff --git a/x-pack/plugins/canvas/public/services/legacy/index.ts b/x-pack/plugins/canvas/public/services/legacy/index.ts index 763fd657ad8004..01f252c8eb0d6e 100644 --- a/x-pack/plugins/canvas/public/services/legacy/index.ts +++ b/x-pack/plugins/canvas/public/services/legacy/index.ts @@ -8,7 +8,6 @@ import { BehaviorSubject } from 'rxjs'; import { CoreSetup, CoreStart, AppUpdater } from '../../../../../../src/core/public'; import { CanvasSetupDeps, CanvasStartDeps } from '../../plugin'; -import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; @@ -17,7 +16,6 @@ import { labsServiceFactory } from './labs'; import { reportingServiceFactory } from './reporting'; export { SearchService } from './search'; -export { PlatformService } from './platform'; export { NavLinkService } from './nav_link'; export { EmbeddablesService } from './embeddables'; export { ExpressionsService } from '../../../../../../src/plugins/expressions/common'; @@ -77,7 +75,6 @@ export type ServiceFromProvider

= P extends CanvasServiceProvider ? export const services = { embeddables: new CanvasServiceProvider(embeddablesServiceFactory), expressions: new CanvasServiceProvider(expressionsServiceFactory), - platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), search: new CanvasServiceProvider(searchServiceFactory), reporting: new CanvasServiceProvider(reportingServiceFactory), @@ -89,7 +86,6 @@ export type CanvasServiceProviders = typeof services; export interface CanvasServices { embeddables: ServiceFromProvider; expressions: ServiceFromProvider; - platform: ServiceFromProvider; navLink: ServiceFromProvider; search: ServiceFromProvider; reporting: ServiceFromProvider; @@ -116,7 +112,6 @@ export const stopServices = () => { export const { embeddables: embeddableService, - platform: platformService, navLink: navLinkService, expressions: expressionsService, search: searchService, diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts index cebefdd7682cc9..9857e27d8a3cf2 100644 --- a/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/legacy/stubs/index.ts @@ -11,7 +11,6 @@ import { expressionsService } from './expressions'; import { reportingService } from './reporting'; import { navLinkService } from './nav_link'; import { labsService } from './labs'; -import { platformService } from './platform'; import { searchService } from './search'; export const stubs: CanvasServices = { @@ -19,7 +18,6 @@ export const stubs: CanvasServices = { expressions: expressionsService, reporting: reportingService, navLink: navLinkService, - platform: platformService, search: searchService, labs: labsService, }; diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts new file mode 100644 index 00000000000000..7a452d809a6146 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/platform.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SavedObjectsStart, + SavedObjectsClientContract, + IUiSettingsClient, + ChromeBreadcrumb, + IBasePath, + ChromeStart, +} from '../../../../../src/core/public'; + +export interface CanvasPlatformService { + getBasePath: () => string; + getBasePathInterface: () => IBasePath; + getDocLinkVersion: () => string; + getElasticWebsiteUrl: () => string; + getHasWriteAccess: () => boolean; + getUISetting: (key: string, defaultValue?: any) => any; + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; + setRecentlyAccessed: (link: string, label: string, id: string) => void; + setFullscreen: ChromeStart['setIsVisible']; + + // TODO: these should go away. We want thin accessors, not entire objects. + // Entire objects are hard to mock, and hide our dependency on the external service. + getSavedObjects: () => SavedObjectsStart; + getSavedObjectsClient: () => SavedObjectsClientContract; + getUISettings: () => IUiSettingsClient; +} diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 5c3440cc4cdbce..1aa05647f7e9ea 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -16,13 +16,16 @@ import { import { CanvasPluginServices } from '..'; import { workpadServiceFactory } from './workpad'; import { notifyServiceFactory } from './notify'; +import { platformServiceFactory } from './platform'; export { workpadServiceFactory } from './workpad'; export { notifyServiceFactory } from './notify'; +export { platformServiceFactory } from './platform'; export const pluginServiceProviders: PluginServiceProviders = { workpad: new PluginServiceProvider(workpadServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), + platform: new PluginServiceProvider(platformServiceFactory), }; export const pluginServiceRegistry = new PluginServiceRegistry( diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts similarity index 72% rename from x-pack/plugins/canvas/public/services/legacy/stubs/platform.ts rename to x-pack/plugins/canvas/public/services/stubs/platform.ts index 5776a1d0d69834..181d355df8a1ce 100644 --- a/x-pack/plugins/canvas/public/services/legacy/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { PlatformService } from '../platform'; +import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; + +import { CanvasPlatformService } from '../platform'; + +type CanvasPlatformServiceFactory = PluginServiceFactory; const noop = (..._args: any[]): any => {}; @@ -15,7 +19,7 @@ const uiSettings: Record = { const getUISetting = (setting: string) => uiSettings[setting]; -export const platformService: PlatformService = { +export const platformServiceFactory: CanvasPlatformServiceFactory = () => ({ getBasePath: () => '/base/path', getBasePathInterface: noop, getDocLinkVersion: () => 'dockLinkVersion', @@ -28,4 +32,4 @@ export const platformService: PlatformService = { getSavedObjectsClient: noop, getUISettings: noop, setFullscreen: noop, -}; +}); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index e4909bdb950812..c652cc573abe9d 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -6,11 +6,12 @@ */ import { get } from 'lodash'; -import { platformService } from '../services'; +import { pluginServices } from '../services'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = (path) => { - const { getHasWriteAccess } = platformService.getService(); + const platformService = pluginServices.getServices().platform; + const { getHasWriteAccess } = platformService; const state = { app: {}, // Kibana stuff in here diff --git a/x-pack/plugins/canvas/public/state/reducers/workpad.js b/x-pack/plugins/canvas/public/state/reducers/workpad.js index acd371e9490fb7..ebde0106f9c012 100644 --- a/x-pack/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/plugins/canvas/public/state/reducers/workpad.js @@ -6,7 +6,7 @@ */ import { handleActions } from 'redux-actions'; -import { platformService } from '../../services'; +import { pluginServices } from '../../services'; import { getDefaultWorkpad } from '../defaults'; import { setWorkpad, @@ -24,9 +24,13 @@ import { APP_ROUTE_WORKPAD } from '../../../common/lib/constants'; export const workpadReducer = handleActions( { [setWorkpad]: (workpadState, { payload }) => { - platformService - .getService() - .setRecentlyAccessed(`${APP_ROUTE_WORKPAD}/${payload.id}`, payload.name, payload.id); + pluginServices + .getServices() + .platform.setRecentlyAccessed( + `${APP_ROUTE_WORKPAD}/${payload.id}`, + payload.name, + payload.id + ); return payload; }, @@ -39,9 +43,13 @@ export const workpadReducer = handleActions( }, [setName]: (workpadState, { payload }) => { - platformService - .getService() - .setRecentlyAccessed(`${APP_ROUTE_WORKPAD}/${workpadState.id}`, payload, workpadState.id); + pluginServices + .getServices() + .platform.setRecentlyAccessed( + `${APP_ROUTE_WORKPAD}/${workpadState.id}`, + payload, + workpadState.id + ); return { ...workpadState, name: payload }; }, diff --git a/x-pack/plugins/data_visualizer/public/api/index.ts b/x-pack/plugins/data_visualizer/public/api/index.ts index 746b43ac86e305..3b96e4caad3403 100644 --- a/x-pack/plugins/data_visualizer/public/api/index.ts +++ b/x-pack/plugins/data_visualizer/public/api/index.ts @@ -8,11 +8,12 @@ import { lazyLoadModules } from '../lazy_load_bundle'; import type { FileDataVisualizerSpec, IndexDataVisualizerSpec } from '../application'; -export async function getFileDataVisualizerComponent(): Promise { +export async function getFileDataVisualizerComponent(): Promise<() => FileDataVisualizerSpec> { const modules = await lazyLoadModules(); - return modules.FileDataVisualizer; + return () => modules.FileDataVisualizer; } -export async function getIndexDataVisualizerComponent(): Promise { + +export async function getIndexDataVisualizerComponent(): Promise<() => IndexDataVisualizerSpec> { const modules = await lazyLoadModules(); - return modules.IndexDataVisualizer; + return () => modules.IndexDataVisualizer; } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/_index.scss b/x-pack/plugins/data_visualizer/public/application/common/components/_index.scss index f57abbbe6396b4..02a8766b3d24c4 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/_index.scss +++ b/x-pack/plugins/data_visualizer/public/application/common/components/_index.scss @@ -1,4 +1,3 @@ @import 'embedded_map/index'; -@import 'experimental_badge/index'; @import 'stats_table/index'; @import 'top_values/top_values'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_experimental_badge.scss b/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_experimental_badge.scss deleted file mode 100644 index 8b21620542ff75..00000000000000 --- a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_experimental_badge.scss +++ /dev/null @@ -1,7 +0,0 @@ -.experimental-badge.euiBetaBadge { - font-size: 10px; - vertical-align: middle; - margin-bottom: 5px; - padding: 0 20px; - line-height: 20px; -} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_index.scss b/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_index.scss deleted file mode 100644 index 9e25affd5e5f60..00000000000000 --- a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'experimental_badge' diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/experimental_badge.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/experimental_badge.tsx deleted file mode 100644 index 9c39ee54a2a868..00000000000000 --- a/x-pack/plugins/data_visualizer/public/application/common/components/experimental_badge/experimental_badge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC } from 'react'; - -import { EuiBetaBadge } from '@elastic/eui'; - -export const ExperimentalBadge: FC<{ tooltipContent: string }> = ({ tooltipContent }) => { - return ( - - - } - tooltipContent={tooltipContent} - /> - - ); -}; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx index 6c9df5cf2eba7b..e43199fabf76c5 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -73,7 +73,12 @@ export const FilebeatConfigFlyout: FC = ({ - + ; + canDisplay(params?: any): Promise; + dataTestSubj?: string; +} + interface Props { fieldStats: FindFileStructureResponse['field_stats']; index: string; @@ -25,6 +38,7 @@ interface Props { timeFieldName?: string; createIndexPattern: boolean; showFilebeatFlyout(): void; + additionalLinks: ResultLink[]; } interface GlobalState { @@ -41,6 +55,7 @@ export const ResultsLinks: FC = ({ timeFieldName, createIndexPattern, showFilebeatFlyout, + additionalLinks, }) => { const { services: { fileUpload }, @@ -55,6 +70,7 @@ export const ResultsLinks: FC = ({ const [discoverLink, setDiscoverLink] = useState(''); const [indexManagementLink, setIndexManagementLink] = useState(''); const [indexPatternManagementLink, setIndexPatternManagementLink] = useState(''); + const [generatedLinks, setGeneratedLinks] = useState>({}); const { services: { @@ -100,6 +116,23 @@ export const ResultsLinks: FC = ({ getDiscoverUrl(); + Promise.all( + additionalLinks.map(async ({ canDisplay, getUrl }) => { + if ((await canDisplay({ indexPatternId })) === false) { + return null; + } + return getUrl({ globalState, indexPatternId }); + }) + ).then((urls) => { + const linksById = urls.reduce((acc, url, i) => { + if (url !== null) { + acc[additionalLinks[i].id] = url; + } + return acc; + }, {} as Record); + setGeneratedLinks(linksById); + }); + if (!unmounted) { setIndexManagementLink( getUrlForApp('management', { path: '/data/index_management/indices' }) @@ -231,6 +264,19 @@ export const ResultsLinks: FC = ({ onClick={showFilebeatFlyout} /> + {additionalLinks + .filter(({ id }) => generatedLinks[id] !== undefined) + .map((link) => ( + + } + data-test-subj="fileDataVisLink" + title={link.title} + description={link.description} + href={generatedLinks[link.id]} + /> + + ))} ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/about_panel/welcome_content.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/about_panel/welcome_content.tsx index 86b869fe06fa18..7b091e699b6172 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/about_panel/welcome_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/about_panel/welcome_content.tsx @@ -7,30 +7,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; - -import { ExperimentalBadge } from '../../../common/components/experimental_badge'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { useDataVisualizerKibana } from '../../../kibana_context'; export const WelcomeContent: FC = () => { - const toolTipContent = i18n.translate( - 'xpack.dataVisualizer.file.welcomeContent.experimentalFeatureTooltip', - { - defaultMessage: "Experimental feature. We'd love to hear your feedback.", - } - ); - const { services: { fileUpload: { getMaxBytesFormatted }, @@ -48,10 +30,7 @@ export const WelcomeContent: FC = () => {

, - }} + defaultMessage="Visualize data from a log file" />

@@ -132,25 +111,6 @@ export const WelcomeContent: FC = () => { />

- - -

- - GitHub - - ), - }} - /> -

-
); diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js index 99b6ef602985f8..054416ad7ba36e 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js @@ -372,6 +372,7 @@ export class FileDataVisualizerView extends Component { hideBottomBar={this.hideBottomBar} savedObjectsClient={this.savedObjectsClient} fileUpload={this.props.fileUpload} + resultsLinks={this.props.resultsLinks} /> {bottomBarVisible && ( diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js index 74a3638f555d02..7e3c6d0c65d3ea 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js @@ -31,7 +31,6 @@ import { addCombinedFieldsToMappings, getDefaultCombinedFields, } from '../../../common/components/combined_fields'; -import { ExperimentalBadge } from '../../../common/components/experimental_badge'; const DEFAULT_TIME_FIELD = '@timestamp'; const DEFAULT_INDEX_SETTINGS = { number_of_shards: 1 }; @@ -510,15 +509,6 @@ export class ImportView extends Component { id="xpack.dataVisualizer.file.importView.importDataTitle" defaultMessage="Import data" /> -   - - } - /> @@ -601,6 +591,7 @@ export class ImportView extends Component { timeFieldName={timeFieldName} createIndexPattern={createIndexPattern} showFilebeatFlyout={this.showFilebeatFlyout} + additionalLinks={this.props.resultsLinks ?? []} /> {isFilebeatFlyoutVisible && ( diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx index b3f7e8531ebf57..3644f7053f1e81 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx @@ -11,9 +11,14 @@ import { getCoreStart, getPluginsStart } from '../../kibana_services'; // @ts-ignore import { FileDataVisualizerView } from './components/file_data_visualizer_view/index'; +import { ResultLink } from '../common/components/results_links'; + +interface Props { + additionalLinks?: ResultLink[]; +} export type FileDataVisualizerSpec = typeof FileDataVisualizer; -export const FileDataVisualizer: FC = () => { +export const FileDataVisualizer: FC = ({ additionalLinks }) => { const coreStart = getCoreStart(); const { data, maps, embeddable, share, security, fileUpload } = getPluginsStart(); const services = { @@ -33,6 +38,7 @@ export const FileDataVisualizer: FC = () => { savedObjectsClient={coreStart.savedObjects.client} http={coreStart.http} fileUpload={fileUpload} + resultsLinks={additionalLinks} /> ); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx index 4b208b0a59ef1d..48410aff545776 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx @@ -18,18 +18,26 @@ import type { IndexPattern } from '../../../../../../../../src/plugins/data/comm import { useDataVisualizerKibana } from '../../../kibana_context'; import { useUrlState } from '../../../common/util/url_state'; import { LinkCard } from '../../../common/components/link_card'; +import { ResultLink } from '../../../common/components/results_links'; interface Props { indexPattern: IndexPattern; searchString?: string | { [key: string]: any }; searchQueryLanguage?: string; + additionalLinks: ResultLink[]; } -// @todo: Add back create job card in a follow up PR -export const ActionsPanel: FC = ({ indexPattern, searchString, searchQueryLanguage }) => { +export const ActionsPanel: FC = ({ + indexPattern, + searchString, + searchQueryLanguage, + additionalLinks, +}) => { const [globalState] = useUrlState('_g'); const [discoverLink, setDiscoverLink] = useState(''); + const [generatedLinks, setGeneratedLinks] = useState>({}); + const { services: { application: { capabilities }, @@ -76,17 +84,56 @@ export const ActionsPanel: FC = ({ indexPattern, searchString, searchQuer } }; + Promise.all( + additionalLinks.map(async ({ canDisplay, getUrl }) => { + if ((await canDisplay({ indexPatternId })) === false) { + return null; + } + return getUrl({ globalState, indexPatternId }); + }) + ).then((urls) => { + const linksById = urls.reduce((acc, url, i) => { + if (url !== null) { + acc[additionalLinks[i].id] = url; + } + return acc; + }, {} as Record); + setGeneratedLinks(linksById); + }); + getDiscoverUrl(); return () => { unmounted = true; }; - }, [indexPattern, searchString, searchQueryLanguage, globalState, capabilities, getUrlGenerator]); + }, [ + indexPattern, + searchString, + searchQueryLanguage, + globalState, + capabilities, + getUrlGenerator, + additionalLinks, + ]); // Note we use display:none for the DataRecognizer section as it needs to be // passed the recognizerResults object, and then run the recognizer check which // controls whether the recognizer section is ultimately displayed. return (
+ {additionalLinks + .filter(({ id }) => generatedLinks[id] !== undefined) + .map((link) => ( + <> + + + + ))} {discoverLink && ( <> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index b116b25670ad27..c9ae3cf7f69a73 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -67,6 +67,7 @@ import { HelpMenu } from '../../../common/components/help_menu'; import { TimeBuckets } from '../../services/time_buckets'; import { extractSearchData } from '../../utils/saved_search_utils'; import { DataVisualizerIndexPatternManagement } from '../index_pattern_management'; +import { ResultLink } from '../../../common/components/results_links'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -120,6 +121,7 @@ export const getDefaultDataVisualizerListState = (): Required = (dataVi dataVisualizerProps.currentSavedSearch ); - const { currentIndexPattern } = dataVisualizerProps; + const { currentIndexPattern, additionalLinks } = dataVisualizerProps; useEffect(() => { if (dataVisualizerProps?.currentSavedSearch !== undefined) { @@ -886,6 +888,7 @@ export const IndexDataVisualizerView: FC = (dataVi indexPattern={currentIndexPattern} searchQueryLanguage={searchQueryLanguage} searchString={searchString} + additionalLinks={additionalLinks ?? []} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index f9e9aece48a060..8e0230a9bc6f94 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -30,15 +30,18 @@ import { } from '../common/util/url_state'; import { useDataVisualizerKibana } from '../kibana_context'; import { IndexPattern } from '../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { ResultLink } from '../common/components/results_links'; export type IndexDataVisualizerSpec = typeof IndexDataVisualizer; export interface DataVisualizerUrlStateContextProviderProps { IndexDataVisualizerComponent: FC; + additionalLinks: ResultLink[]; } export const DataVisualizerUrlStateContextProvider: FC = ({ IndexDataVisualizerComponent, + additionalLinks, }) => { const { services: { @@ -168,6 +171,7 @@ export const DataVisualizerUrlStateContextProvider: FC ) : (
@@ -176,7 +180,7 @@ export const DataVisualizerUrlStateContextProvider: FC { +export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ additionalLinks }) => { const coreStart = getCoreStart(); const { data, @@ -204,6 +208,7 @@ export const IndexDataVisualizer: FC = () => { ); diff --git a/x-pack/plugins/data_visualizer/public/index.ts b/x-pack/plugins/data_visualizer/public/index.ts index b0a622dfe490b2..1a045f144c0152 100644 --- a/x-pack/plugins/data_visualizer/public/index.ts +++ b/x-pack/plugins/data_visualizer/public/index.ts @@ -13,4 +13,9 @@ export function plugin() { export { DataVisualizerPluginStart } from './plugin'; -export type { IndexDataVisualizerViewProps } from './application'; +export type { + FileDataVisualizerSpec, + IndexDataVisualizerSpec, + IndexDataVisualizerViewProps, +} from './application'; +export type { ResultLink } from './application/common/components/results_links'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 11003d0fcc1711..1b5dab08396636 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + import { HttpResponse } from 'src/core/public'; import { FlashMessagesLogic } from './flash_messages_logic'; @@ -31,12 +33,17 @@ interface Options { isQueued?: boolean; } +export const defaultErrorMessage = i18n.translate( + 'xpack.enterpriseSearch.shared.flashMessages.defaultErrorMessage', + { + defaultMessage: 'An unexpected error occurred', + } +); + /** * Converts API/HTTP errors into user-facing Flash Messages */ export const flashAPIErrors = (error: HttpResponse, { isQueued }: Options = {}) => { - const defaultErrorMessage = 'An unexpected error occurred'; - const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors) ? error.body!.attributes.errors.map((message) => ({ type: 'error', message })) : [{ type: 'error', message: error?.body?.message || defaultErrorMessage }]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index fd1a574b3438f6..3e8322145dad61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -27,6 +27,7 @@ const spyScrollTo = jest.fn(); Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { defaultErrorMessage } from '../../../../../shared/flash_messages/handle_api_errors'; import { SchemaType } from '../../../../../shared/schema/types'; import { AppLogic } from '../../../../app_logic'; @@ -390,13 +391,25 @@ describe('SchemaLogic', () => { expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); - it('handles error', async () => { + it('handles error with message', async () => { + const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors'); + // We expect body.message to be a string[] when it is present + http.post.mockReturnValue(Promise.reject({ body: { message: ['this is an error'] } })); + SchemaLogic.actions.setServerField(schema, ADD); + await nextTick(); + + expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith(['this is an error']); + expect(spyScrollTo).toHaveBeenCalledWith(0, 0); + }); + + it('handles error with no message', async () => { const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors'); - http.post.mockReturnValue(Promise.reject({ message: 'this is an error' })); + http.post.mockReturnValue(Promise.reject()); SchemaLogic.actions.setServerField(schema, ADD); await nextTick(); - expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith('this is an error'); + expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith([defaultErrorMessage]); + expect(spyScrollTo).toHaveBeenCalledWith(0, 0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index b2c329f0544fd3..7af074d412a605 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -17,6 +17,7 @@ import { setErrorMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; +import { defaultErrorMessage } from '../../../../../shared/flash_messages/handle_api_errors'; import { HttpLogic } from '../../../../../shared/http'; import { IndexJob, @@ -349,7 +350,9 @@ export const SchemaLogic = kea>({ } catch (e) { window.scrollTo(0, 0); if (isAdding) { - actions.onSchemaSetFormErrors(e?.message); + // We expect body.message to be a string[] for actions.onSchemaSetFormErrors + const message: string[] = e?.body?.message || [defaultErrorMessage]; + actions.onSchemaSetFormErrors(message); } else { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 0cb3f77c681a13..78e4954a4bf16e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -30,6 +30,11 @@ const activityFeed = [ target: 'http://localhost:3002/ws/org/sources', timestamp: '2020-06-24 16:34:16', }, + { + id: '(foo@example.com)', + message: 'joined the organization', + timestamp: '2021-07-02 16:38:27', + }, ]; describe('RecentActivity', () => { @@ -46,13 +51,14 @@ describe('RecentActivity', () => { it('renders an activityFeed with links', () => { setMockValues({ activityFeed }); const wrapper = shallow(); - const activity = wrapper.find(RecentActivityItem).dive(); + const sourceActivityItem = wrapper.find(RecentActivityItem).first().dive(); + const newUserActivityItem = wrapper.find(RecentActivityItem).last().dive(); - expect(activity).toHaveLength(1); - - const link = activity.find('[data-test-subj="viewSourceDetailsLink"]'); + const link = sourceActivityItem.find('[data-test-subj="viewSourceDetailsLink"]'); link.simulate('click'); expect(mockTelemetryActions.sendWorkplaceSearchTelemetry).toHaveBeenCalled(); + + expect(newUserActivityItem.find('[data-test-subj="newUserTextWrapper"]')).toHaveLength(1); }); it('renders activity item error state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 8bda7c2843b9e9..51a6508986037d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -29,7 +29,7 @@ export interface FeedActivity { id: string; message: string; timestamp: string; - sourceId: string; + sourceId?: string; } export const RecentActivity: React.FC = () => { @@ -98,23 +98,29 @@ export const RecentActivityItem: React.FC = ({ return (
- - {id} {message} - {status === 'error' && ( - - {' '} - - - )} - + {sourceId ? ( + + {id} {message} + {status === 'error' && ( + + {' '} + + + )} + + ) : ( +
+ {id} {message} +
+ )}
{moment.utc(timestamp).fromNow()}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 8be6232733defc..5ac594842d3925 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -221,7 +221,7 @@ export const FleetAppContext: React.FC<{ const unlistenParentHistory = history.listen(() => { const newHash = createHashHistory(); if (newHash.location.pathname !== routerHistoryInstance.location.pathname) { - routerHistoryInstance.replace(newHash.location.pathname); + routerHistoryInstance.replace(newHash.location.pathname + newHash.location.search || ''); } }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 1363af573b86d6..5075398d2d6b09 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -425,7 +425,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId] ); - const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); + const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); const stepConfigurePackagePolicy = useMemo( () => @@ -444,7 +444,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { /> {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} - {!ExtensionView && ( + {!extensionView && ( { )} {/* If an Agent Policy and a package has been selected, then show UI extension (if any) */} - {ExtensionView && packagePolicy.policy_id && packagePolicy.package?.name && ( + {extensionView && packagePolicy.policy_id && packagePolicy.package?.name && ( - + )} @@ -474,7 +477,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { validationResults, formState, integrationInfo?.name, - ExtensionView, + extensionView, handleExtensionViewOnChange, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index fb3603aaef7f4d..b07d76dc6bd8e8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -338,7 +338,7 @@ export const EditPackagePolicyForm = memo<{ packageInfo, }; - const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); + const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); const configurePackage = useMemo( () => @@ -354,7 +354,7 @@ export const EditPackagePolicyForm = memo<{ /> {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} - {!ExtensionView && ( + {!extensionView && ( )} - {ExtensionView && + {extensionView && packagePolicy.policy_id && packagePolicy.package?.name && originalPackagePolicy && ( - { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ + ".\\\\elastic-agent.exe install -f \` + --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1" `); }); @@ -78,9 +78,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + ".\\\\elastic-agent.exe install -f \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1" `); }); @@ -137,14 +137,14 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ - --fleet-server-policy=policy-1 \\\\ - --certificate-authorities= \\\\ - --fleet-server-es-ca= \\\\ - --fleet-server-cert= \\\\ + ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` + -f \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` + --fleet-server-policy=policy-1 \` + --certificate-authorities= \` + --fleet-server-es-ca= \` + --fleet-server-cert= \` --fleet-server-cert-key=" `); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts index b91c4b60aa7138..e129d7a4d5b4e5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -16,22 +16,23 @@ export function getInstallCommandForPlatform( isProductionDeployment?: boolean ) { let commandArguments = ''; + const newLineSeparator = platform === 'windows' ? '`' : '\\'; if (isProductionDeployment && fleetServerHost) { - commandArguments += `--url=${fleetServerHost} \\\n`; + commandArguments += `--url=${fleetServerHost} ${newLineSeparator}\n`; } - commandArguments += ` -f \\\n --fleet-server-es=${esHost}`; - commandArguments += ` \\\n --fleet-server-service-token=${serviceToken}`; + commandArguments += ` -f ${newLineSeparator}\n --fleet-server-es=${esHost}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; if (policyId) { - commandArguments += ` \\\n --fleet-server-policy=${policyId}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-policy=${policyId}`; } if (isProductionDeployment) { - commandArguments += ` \\\n --certificate-authorities=`; - commandArguments += ` \\\n --fleet-server-es-ca=`; - commandArguments += ` \\\n --fleet-server-cert=`; - commandArguments += ` \\\n --fleet-server-cert-key=`; + commandArguments += ` ${newLineSeparator}\n --certificate-authorities=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-es-ca=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-cert=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-cert-key=`; } switch (platform) { diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index cad51a54d70744..ae59d33e44b82f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -196,7 +196,7 @@ export const IntegrationsAppContext: React.FC<{ const unlistenParentHistory = history.listen(() => { const newHash = createHashHistory(); if (newHash.location.pathname !== routerHistoryInstance.location.pathname) { - routerHistoryInstance.replace(newHash.location.pathname); + routerHistoryInstance.replace(newHash.location.pathname + newHash.location.search || ''); } }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx index 41db09b0538b91..f8e4c9994e5707 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx @@ -19,7 +19,7 @@ export const DisplayedAssets: ServiceNameToAssetTypes = { elasticsearch: Object.values(ElasticsearchAssetType), }; -export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType; +export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view'; export const AssetTitleMap: Record = { dashboard: 'Dashboard', @@ -36,6 +36,7 @@ export const AssetTitleMap: Record = { lens: 'Lens', security_rule: 'Security Rule', ml_module: 'ML Module', + view: 'Views', }; export const ServiceTitleMap: Record = { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx index e6dce1bc513674..6d075faeef308a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx @@ -10,12 +10,17 @@ import { Redirect } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { Loading, Error } from '../../../../../components'; +import { Loading, Error, ExtensionWrapper } from '../../../../../components'; import type { PackageInfo } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; -import { useGetPackageInstallStatus, useLink, useStartServices } from '../../../../../hooks'; +import { + useGetPackageInstallStatus, + useLink, + useStartServices, + useUIExtension, +} from '../../../../../hooks'; import type { AssetSavedObject } from './types'; import { allowedAssetTypes } from './constants'; @@ -27,9 +32,12 @@ interface AssetsPanelProps { export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { const { name, version } = packageInfo; + const pkgkey = `${name}-${version}`; + const { savedObjects: { client: savedObjectsClient }, } = useStartServices(); + const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets'); const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); @@ -76,13 +84,7 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) { - return ( - - ); + return ; } let content: JSX.Element | Array; @@ -102,31 +104,49 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { /> ); } else if (assetSavedObjects === undefined) { - content = ( - -

- -

-
- ); + if (customAssetsExtension) { + // If a UI extension for custom asset entries is defined, render the custom component here depisite + // there being no saved objects found + content = ( + + + + ); + } else { + content = ( + +

+ +

+
+ ); + } } else { - content = allowedAssetTypes.map((assetType) => { - const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType); + content = [ + ...allowedAssetTypes.map((assetType) => { + const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType); - if (!sectionAssetSavedObjects.length) { - return null; - } + if (!sectionAssetSavedObjects.length) { + return null; + } - return ( - <> - - - - ); - }); + return ( + <> + + + + ); + }), + // Ensure we add any custom assets provided via UI extension to the end of the list of other assets + customAssetsExtension ? ( + + + + ) : null, + ]; } return ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx index 12d4a0014b976b..91c6b68c662214 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx @@ -55,6 +55,11 @@ export const AssetsAccordion: FunctionComponent = ({ savedObjects, type } {savedObjects.map(({ id, attributes: { title, description } }, idx) => { + // Ignore custom asset views + if (type === 'view') { + return; + } + const pathToObjectInApp = getHrefToObjectInKibanaApp({ http, id, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts index 21efd1cd562e80..26a6729be36232 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts @@ -17,4 +17,4 @@ export type AllowedAssetTypes = [ KibanaAssetType.visualization ]; -export type AllowedAssetType = AllowedAssetTypes[number]; +export type AllowedAssetType = AllowedAssetTypes[number] | 'view'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom/custom.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom/custom.tsx index ef8d21236c6704..b59804cea17335 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom/custom.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom/custom.tsx @@ -18,16 +18,16 @@ interface Props { } export const CustomViewPage: React.FC = memo(({ packageInfo }) => { - const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom'); + const customViewExtension = useUIExtension(packageInfo.name, 'package-detail-custom'); const { getPath } = useLink(); const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]); - return CustomView ? ( + return customViewExtension ? ( - + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 04253994d3875e..f436c248abd3c3 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -113,7 +113,7 @@ describe('when on integration detail', () => { }); }); - describe('and a custom UI extension is registered', () => { + describe('and a custom tab UI extension is registered', () => { // Because React Lazy components are loaded async (Promise), we setup this "watcher" Promise // that is `resolved` once the lazy components actually renders. let lazyComponentWasRendered: Promise; @@ -136,7 +136,7 @@ describe('when on integration detail', () => { testRenderer.startInterface.registerExtension({ package: 'nginx', view: 'package-detail-custom', - component: CustomComponent, + Component: CustomComponent, }); render(); @@ -162,6 +162,53 @@ describe('when on integration detail', () => { }); }); + describe('and a custom assets UI extension is registered', () => { + let lazyComponentWasRendered: Promise; + + beforeEach(() => { + let setWasRendered: () => void; + lazyComponentWasRendered = new Promise((resolve) => { + setWasRendered = resolve; + }); + + const CustomComponent = lazy(async () => { + return { + default: memo(() => { + setWasRendered(); + return
hello
; + }), + }; + }); + + testRenderer.startInterface.registerExtension({ + package: 'nginx', + view: 'package-detail-assets', + Component: CustomComponent, + }); + + render(); + }); + + afterEach(() => { + // @ts-ignore + lazyComponentWasRendered = undefined; + }); + + it('should display "assets" tab in navigation', () => { + expect(renderResult.getByTestId('tab-assets')); + }); + + it('should display custom assets when tab is clicked', async () => { + act(() => { + testRenderer.history.push( + pagePathGetters.integration_details_assets({ pkgkey: 'nginx-0.3.7' })[1] + ); + }); + await lazyComponentWasRendered; + expect(renderResult.getByTestId('custom-hello')); + }); + }); + describe('and the Add integration button is clicked', () => { beforeEach(() => render()); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 2102c5055503b9..21a139ad11baab 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -101,6 +101,8 @@ export function Detail() { const setPackageInstallStatus = useSetPackageInstallStatus(); const getPackageInstallStatus = useGetPackageInstallStatus(); + const CustomAssets = useUIExtension(packageInfo?.name ?? '', 'package-detail-assets'); + const packageInstallStatus = useMemo(() => { if (packageInfo === null || !packageInfo.name) { return undefined; @@ -418,7 +420,7 @@ export function Detail() { }); } - if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) { + if (packageInstallStatus === InstallStatus.installed && (packageInfo.assets || CustomAssets)) { tabs.push({ id: 'assets', name: ( @@ -471,7 +473,7 @@ export function Detail() { } return tabs; - }, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab]); + }, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab, CustomAssets]); return ( ) => { setPagination({ @@ -98,35 +105,46 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps [setPagination] ); - const renderViewDataStepContent = useCallback( - () => ( - <> - - - {i18n.translate( - 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel', - { defaultMessage: 'Please note' } - )} - - ), - }} - /> - - - - {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { - defaultMessage: 'View assets', - })} - - - ), - [name, version, getHref] - ); + const viewDataStep = useMemo(() => { + if (agentEnrollmentFlyoutExtension) { + return { + title: agentEnrollmentFlyoutExtension.title, + children: , + }; + } + + return { + title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', { + defaultMessage: 'View your data', + }), + children: ( + <> + + + {i18n.translate( + 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel', + { defaultMessage: 'Please note' } + )} + + ), + }} + /> + + + + {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { + defaultMessage: 'View assets', + })} + + + ), + }; + }, [name, version, getHref, agentEnrollmentFlyoutExtension]); const columns: Array> = useMemo( () => [ @@ -230,13 +248,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); }, }, ], - [renderViewDataStepContent] + [viewDataStep] ); const noItemsMessage = useMemo(() => { @@ -292,7 +310,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) ?.agentPolicy } - viewDataStepContent={renderViewDataStepContent()} + viewDataStep={viewDataStep} /> )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index f6d62d45a4e56d..f68b1b878c51c0 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks'; import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page/components'; -import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps'; +import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps'; import type { Props } from '.'; import { AgentEnrollmentFlyout } from '.'; @@ -129,24 +129,26 @@ describe('', () => { }); }); - describe('"View data" extension point', () => { - it('calls the "View data" step when UI extension is provided', async () => { + // Skipped due to implementation details in the step components. See https://github.com/elastic/kibana/issues/103894 + describe.skip('"View data" extension point', () => { + it('shows the "View data" step when UI extension is provided', async () => { jest.clearAllMocks(); await act(async () => { testBed = await setup({ agentPolicies: [], onClose: jest.fn(), - viewDataStepContent:
, + viewDataStep: { title: 'View Data', children:
}, }); testBed.component.update(); }); const { exists, actions } = testBed; expect(exists('agentEnrollmentFlyout')).toBe(true); - expect(ViewDataStep).toHaveBeenCalled(); + expect(exists('view-data-step')).toBe(true); jest.clearAllMocks(); actions.goToStandaloneTab(); - expect(ViewDataStep).not.toHaveBeenCalled(); + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(exists('view-data-step')).toBe(false); }); it('does not call the "View data" step when UI extension is not provided', async () => { @@ -155,17 +157,17 @@ describe('', () => { testBed = await setup({ agentPolicies: [], onClose: jest.fn(), - viewDataStepContent: undefined, + viewDataStep: undefined, }); testBed.component.update(); }); const { exists, actions } = testBed; expect(exists('agentEnrollmentFlyout')).toBe(true); - expect(ViewDataStep).not.toHaveBeenCalled(); + expect(exists('view-data-step')).toBe(false); jest.clearAllMocks(); actions.goToStandaloneTab(); - expect(ViewDataStep).not.toHaveBeenCalled(); + expect(exists('view-data-step')).toBe(false); }); }); }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 08d78154941e9a..9b82b2a80b5e14 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -45,7 +45,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentPolicy, agentPolicies, - viewDataStepContent, + viewDataStep, defaultMode = 'managed', }) => { const [mode, setMode] = useState(defaultMode); @@ -119,14 +119,10 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ ) : ( - + )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index c739725b797393..61f86335cd7f93 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -23,12 +23,7 @@ import { } from '../../applications/fleet/sections/agents/agent_requirements_page/components'; import { FleetServerRequirementPage } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { - DownloadStep, - AgentPolicySelectionStep, - AgentEnrollmentKeySelectionStep, - ViewDataStep, -} from './steps'; +import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps'; import type { BaseProps } from './types'; type Props = BaseProps; @@ -61,7 +56,7 @@ const FleetServerMissingRequirements = () => { }; export const ManagedInstructions = React.memo( - ({ agentPolicy, agentPolicies, viewDataStepContent }) => { + ({ agentPolicy, agentPolicies, viewDataStep }) => { const fleetStatus = useFleetStatus(); const [selectedApiKeyId, setSelectedAPIKeyId] = useState(); @@ -118,8 +113,8 @@ export const ManagedInstructions = React.memo( }); } - if (viewDataStepContent) { - baseSteps.push(ViewDataStep(viewDataStepContent)); + if (viewDataStep) { + baseSteps.push({ 'data-test-subj': 'view-data-step', ...viewDataStep }); } return baseSteps; @@ -127,12 +122,12 @@ export const ManagedInstructions = React.memo( agentPolicy, selectedApiKeyId, setSelectedAPIKeyId, - viewDataStepContent, agentPolicies, apiKey.data, fleetServerSteps, isFleetServerPolicySelected, settings.data?.item?.fleet_server_hosts, + viewDataStep, ]); return ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index f77cba754e909f..8eeb5fac4b0df2 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -144,16 +144,3 @@ export const AgentEnrollmentKeySelectionStep = ({ ), }; }; - -/** - * Send users to assets installed by the package in Kibana so they can - * view their data. - */ -export const ViewDataStep = (content: JSX.Element) => { - return { - title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', { - defaultMessage: 'View your data', - }), - children: content, - }; -}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index e0c5b040a61fb2..9ee514c634655f 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { EuiStepProps } from '@elastic/eui'; + import type { AgentPolicy } from '../../types'; export interface BaseProps { @@ -24,5 +26,5 @@ export interface BaseProps { * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI * in some way. This is an area for consumers to render a button and text explaining how data can be viewed. */ - viewDataStepContent?: JSX.Element; + viewDataStep?: EuiStepProps; } diff --git a/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx b/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx new file mode 100644 index 00000000000000..f4dd3a7deaaab1 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FunctionComponent } from 'react'; +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiNotificationBadge, + EuiSpacer, + EuiSplitPanel, + EuiLink, + EuiHorizontalRule, +} from '@elastic/eui'; + +import { AssetTitleMap } from '../applications/integrations/sections/epm/constants'; +import { useStartServices } from '../hooks'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; + +export interface CustomAssetsAccordionProps { + views: Array<{ + name: string; + url: string; + description: string; + }>; + initialIsOpen?: boolean; +} + +export const CustomAssetsAccordion: FunctionComponent = ({ + views, + initialIsOpen = false, +}) => { + const { application } = useStartServices(); + + return ( + + + +

{AssetTitleMap.view}

+
+
+ + +

{views.length}

+
+
+ + } + id="custom-assets" + > + <> + + + {views.map((view, index) => ( + <> + + +

+ + {view.name} + +

+
+ + + +

{view.description}

+
+
+ {index + 1 < views.length && } + + ))} +
+ +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 1f64de27fce392..9743135d5f1c10 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useState } from 'react'; import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import type { AgentPolicy, PackagePolicy } from '../types'; @@ -21,8 +22,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; - viewDataStepContent?: JSX.Element; -}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => { + viewDataStep?: EuiStepProps; +}> = ({ agentPolicy, packagePolicy, viewDataStep }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -106,7 +107,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ diff --git a/x-pack/plugins/fleet/public/constants/index.ts b/x-pack/plugins/fleet/public/constants/index.ts index a7cc2726eace1d..a0e88bc58726a9 100644 --- a/x-pack/plugins/fleet/public/constants/index.ts +++ b/x-pack/plugins/fleet/public/constants/index.ts @@ -24,3 +24,5 @@ export { export * from './page_paths'; export const INDEX_NAME = '.kibana'; + +export const CUSTOM_LOGS_INTEGRATION_NAME = 'log'; diff --git a/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx b/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx new file mode 100644 index 00000000000000..4090c4520bf2a9 --- /dev/null +++ b/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { CustomAssetsAccordion } from './components/custom_assets_accordion'; +import type { CustomAssetsAccordionProps } from './components/custom_assets_accordion'; +import { useStartServices } from './hooks'; +import type { PackageAssetsComponent } from './types'; + +export const CustomLogsAssetsExtension: PackageAssetsComponent = () => { + const { http } = useStartServices(); + const logStreamUrl = http.basePath.prepend('/app/logs/stream'); + + const views: CustomAssetsAccordionProps['views'] = [ + { + name: i18n.translate('xpack.fleet.assets.customLogs.name', { defaultMessage: 'Logs' }), + url: logStreamUrl, + description: i18n.translate('xpack.fleet.assets.customLogs.description', { + defaultMessage: 'View Custom logs data in Logs app', + }), + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts b/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts index 3880a08a7ec036..93e316be241cb5 100644 --- a/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts +++ b/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts @@ -20,7 +20,7 @@ type NarrowExtensionPoint( packageName: UIExtensionPoint['package'], view: V -): NarrowExtensionPoint['component'] | undefined => { +): NarrowExtensionPoint | undefined => { const registeredExtensions = useContext(UIExtensionsContext); if (!registeredExtensions) { @@ -32,6 +32,6 @@ export const useUIExtension = (async () => { + return { + default: CustomLogsAssetsExtension, + }; +}); diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index c092d5914637cb..9b3aefa488f7b9 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -32,7 +32,7 @@ import type { CheckPermissionsResponse, PostIngestSetupResponse } from '../commo import type { FleetConfigType } from '../common/types'; -import { FLEET_BASE_PATH } from './constants'; +import { CUSTOM_LOGS_INTEGRATION_NAME, FLEET_BASE_PATH } from './constants'; import { licenseService } from './hooks'; import { setHttpClient } from './hooks/use_request'; import { createPackageSearchProvider } from './search_provider'; @@ -43,6 +43,7 @@ import { } from './components/home_integration'; import { createExtensionRegistrationCallback } from './services/ui_extensions'; import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types'; +import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; export { FleetConfigType } from '../common/types'; @@ -204,6 +205,13 @@ export class FleetPlugin implements Plugin; + const registerExtension = createExtensionRegistrationCallback(this.extensions); + + registerExtension({ + package: CUSTOM_LOGS_INTEGRATION_NAME, + view: 'package-detail-assets', + Component: LazyCustomLogsAssetsExtension, + }); return { isInitialized: () => { @@ -229,8 +237,7 @@ export class FleetPlugin implements Plugin { register({ view: 'package-policy-edit', package: 'endpoint', - component: LazyCustomView, + Component: LazyCustomView, }); expect(storage.endpoint['package-policy-edit']).toEqual({ view: 'package-policy-edit', package: 'endpoint', - component: LazyCustomView, + Component: LazyCustomView, }); }); @@ -57,21 +57,21 @@ describe('UI Extension services', () => { register({ view: 'package-policy-edit', package: 'endpoint', - component: LazyCustomView, + Component: LazyCustomView, }); expect(() => { register({ view: 'package-policy-edit', package: 'endpoint', - component: LazyCustomView2, + Component: LazyCustomView2, }); }).toThrow(); expect(storage.endpoint['package-policy-edit']).toEqual({ view: 'package-policy-edit', package: 'endpoint', - component: LazyCustomView, + Component: LazyCustomView, }); }); }); diff --git a/x-pack/plugins/fleet/public/types/ui_extensions.ts b/x-pack/plugins/fleet/public/types/ui_extensions.ts index c79b676a26b332..bc692fe1caa7dd 100644 --- a/x-pack/plugins/fleet/public/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/types/ui_extensions.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { EuiStepProps } from '@elastic/eui'; import type { ComponentType, LazyExoticComponent } from 'react'; import type { NewPackagePolicy, PackageInfo, PackagePolicy } from './index'; @@ -48,7 +49,7 @@ export interface PackagePolicyEditExtensionComponentProps { export interface PackagePolicyEditExtension { package: string; view: 'package-policy-edit'; - component: LazyExoticComponent; + Component: LazyExoticComponent; } /** @@ -76,7 +77,7 @@ export interface PackagePolicyCreateExtensionComponentProps { export interface PackagePolicyCreateExtension { package: string; view: 'package-policy-create'; - component: LazyExoticComponent; + Component: LazyExoticComponent; } /** @@ -94,11 +95,32 @@ export interface PackageCustomExtensionComponentProps { export interface PackageCustomExtension { package: string; view: 'package-detail-custom'; - component: LazyExoticComponent; + Component: LazyExoticComponent; +} + +/** + * UI Component Extension for displaying custom views under the Assets tab for a given Integration + */ +export type PackageAssetsComponent = ComponentType<{}>; + +/** Extension point registration contract for Integration details Assets view */ +export interface PackageAssetsExtension { + package: string; + view: 'package-detail-assets'; + Component: LazyExoticComponent; +} + +export interface AgentEnrollmentFlyoutFinalStepExtension { + package: string; + view: 'agent-enrollment-flyout'; + title: EuiStepProps['title']; + Component: ComponentType<{}>; } /** Fleet UI Extension Point */ export type UIExtensionPoint = | PackagePolicyEditExtension | PackageCustomExtension - | PackagePolicyCreateExtension; + | PackagePolicyCreateExtension + | PackageAssetsExtension + | AgentEnrollmentFlyoutFinalStepExtension; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index cff70737be6ee1..83029833163161 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -773,10 +773,10 @@ class AgentPolicyService { ) { const names: string[] = []; if (fullAgentPolicy.agent.monitoring.logs) { - names.push(`logs-elastic_agent.*-${monitoringNamespace}`); + names.push(`logs-elastic_agent*-${monitoringNamespace}`); } if (fullAgentPolicy.agent.monitoring.metrics) { - names.push(`metrics-elastic_agent.*-${monitoringNamespace}`); + names.push(`metrics-elastic_agent*-${monitoringNamespace}`); } permissions._elastic_agent_checks.indices = [ diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index a6f499ede90381..b83843f0c615d9 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -76,26 +76,33 @@ async function fetchIndicesCall( query: catQuery, }); - // The two responses should be equal in the number of indices returned - return catHits.map((hit) => { + // System indices may show up in _cat APIs, as these APIs are primarily used for troubleshooting + // For now, we filter them out and only return index information for the indices we have + // In the future, we should migrate away from using cat APIs (https://github.com/elastic/kibana/issues/57286) + return catHits.reduce((decoratedIndices, hit) => { const index = indices[hit.index]; - const aliases = Object.keys(index.aliases); - return { - health: hit.health, - status: hit.status, - name: hit.index, - uuid: hit.uuid, - primary: hit.pri, - replica: hit.rep, - documents: hit['docs.count'], - size: hit['store.size'], - isFrozen: hit.sth === 'true', // sth value coming back as a string from ES - aliases: aliases.length ? aliases : 'none', - hidden: index.settings.index.hidden === 'true', - data_stream: index.data_stream, - }; - }); + if (typeof index !== 'undefined') { + const aliases = Object.keys(index.aliases); + + decoratedIndices.push({ + health: hit.health, + status: hit.status, + name: hit.index, + uuid: hit.uuid, + primary: hit.pri, + replica: hit.rep, + documents: hit['docs.count'], + size: hit['store.size'], + isFrozen: hit.sth === 'true', // sth value coming back as a string from ES + aliases: aliases.length ? aliases : 'none', + hidden: index.settings.index.hidden === 'true', + data_stream: index.data_stream, + }); + } + + return decoratedIndices; + }, [] as Index[]); } export const fetchIndices = async ( diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1fd12460ba3b65..12522402116c93 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -161,6 +161,7 @@ export async function mountApp( embeddableId: isCopied ? undefined : embeddableEditorIncomingState.embeddableId, type: LENS_EMBEDDABLE_TYPE, input, + searchSessionId: data.search.session.getSessionId(), }, }); } else { @@ -178,6 +179,10 @@ export async function mountApp( if (!initialContext) { data.query.filterManager.setAppFilters([]); } + + if (embeddableEditorIncomingState?.searchSessionId) { + data.search.session.continue(embeddableEditorIncomingState.searchSessionId); + } const { datasourceMap, visualizationMap } = instance; const initialDatasourceId = getInitialDatasourceId(datasourceMap); @@ -298,6 +303,7 @@ export async function mountApp( params.element ); return () => { + data.search.session.clear(); unmountComponentAtNode(params.element); unlistenParentHistory(); lensStore.dispatch(navigateAway()); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 3f539b18896c4e..e6cba7ac9dce08 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -126,7 +126,7 @@ export const counterRateOperation: OperationDefinition< signature: i18n.translate('xpack.lens.indexPattern.counterRate.signature', { defaultMessage: 'metric: number', }), - description: i18n.translate('xpack.lens.indexPattern.counterRate.documentation', { + description: i18n.translate('xpack.lens.indexPattern.counterRate.documentation.markdown', { defaultMessage: ` Calculates the rate of an ever increasing counter. This function will only yield helpful results on counter metric fields which contain a measurement of some kind monotonically growing over time. If the value does get smaller, it will interpret this as a counter reset. To get most precise results, \`counter_rate\` should be calculated on the \`max\` of a field. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 0bdef4cc7678de..9c8437140f7939 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -122,7 +122,7 @@ export const cumulativeSumOperation: OperationDefinition< signature: i18n.translate('xpack.lens.indexPattern.cumulative_sum.signature', { defaultMessage: 'metric: number', }), - description: i18n.translate('xpack.lens.indexPattern.cumulativeSum.documentation', { + description: i18n.translate('xpack.lens.indexPattern.cumulativeSum.documentation.markdown', { defaultMessage: ` Calculates the cumulative sum of a metric over time, adding all previous values of a series to each value. To use this function, you need to configure a date histogram dimension as well. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 857b719b0f4114..8890390378d216 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -114,7 +114,7 @@ export const derivativeOperation: OperationDefinition< signature: i18n.translate('xpack.lens.indexPattern.differences.signature', { defaultMessage: 'metric: number', }), - description: i18n.translate('xpack.lens.indexPattern.differences.documentation', { + description: i18n.translate('xpack.lens.indexPattern.differences.documentation.markdown', { defaultMessage: ` Calculates the difference to the last value of a metric over time. To use this function, you need to configure a date histogram dimension as well. Differences requires the data to be sequential. If your data is empty when using differences, try increasing the date histogram interval. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index aea203c70a928d..72e14cc2ea016b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -137,7 +137,7 @@ export const movingAverageOperation: OperationDefinition< signature: i18n.translate('xpack.lens.indexPattern.moving_average.signature', { defaultMessage: 'metric: number, [window]: number', }), - description: i18n.translate('xpack.lens.indexPattern.movingAverage.documentation', { + description: i18n.translate('xpack.lens.indexPattern.movingAverage.documentation.markdown', { defaultMessage: ` Calculates the moving average of a metric over time, averaging the last n-th values to calculate the current value. To use this function, you need to configure a date histogram dimension as well. The default window value is {defaultValue}. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx index 21ec5387b38539..4250811e114527 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/overall_metric.tsx @@ -115,7 +115,7 @@ export const overallSumOperation = buildOverallMetricOperation ), }, @@ -102,8 +105,10 @@ count(kql='response.status_code > 400') / count() }), description: ( ), }, @@ -126,8 +132,10 @@ percentile(system.network.in.bytes, percentile=99, shift='1w') }), description: ( ), }, @@ -372,9 +381,8 @@ sum(products.base_price) / overall_sum(sum(products.base_price)) }} > >({ signature: i18n.translate('xpack.lens.indexPattern.metric.signature', { defaultMessage: 'field: string', }), - description: i18n.translate('xpack.lens.indexPattern.metric.documentation', { + description: i18n.translate('xpack.lens.indexPattern.metric.documentation.markdown', { defaultMessage: ` Returns the {metric} of a field. This function only works for number fields. diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index bdc4743e4f56da..9c9f7e8b66a1f2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -220,7 +220,7 @@ export const percentileOperation: OperationDefinition { minLon: 95, }, buffer: { - maxLat: 21.94305, - maxLon: 112.5, + maxLat: 11.1784, + maxLon: 101.25, minLat: 0, minLon: 90, }, @@ -158,10 +158,10 @@ describe('map_actions', () => { minLon: 85, }, buffer: { - maxLat: 7.71099, - maxLon: 92.8125, - minLat: -2.81137, - minLon: 82.26563, + maxLat: 5.26601, + maxLon: 90.35156, + minLat: -0.35156, + minLon: 84.72656, }, }, type: 'MAP_EXTENT_CHANGED', @@ -190,10 +190,10 @@ describe('map_actions', () => { minLon: 96, }, buffer: { - maxLat: 13.58192, - maxLon: 103.53516, - minLat: 3.33795, - minLon: 93.33984, + maxLat: 11.0059, + maxLon: 101.07422, + minLat: 5.96575, + minLon: 95.97656, }, }, type: 'MAP_EXTENT_CHANGED', diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index a1c7e2f7b453cc..af8d6cc6bedc3c 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -60,7 +60,6 @@ import { Timeslice, } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; -import { scaleBounds } from '../../common/elasticsearch_util'; import { cleanTooltipStateForLayer } from './tooltip_actions'; import { VectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; @@ -167,9 +166,8 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { } if (!doesBufferContainExtent || currentZoom !== newZoom) { - const expandedExtent = scaleBounds(extent, 0.5); // snap to the smallest tile-bounds, to avoid jitter in the bounds - dataFilters.buffer = expandToTileBoundaries(expandedExtent, Math.ceil(newZoom)); + dataFilters.buffer = expandToTileBoundaries(extent, Math.ceil(newZoom)); } } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 3b3b1af30610d6..f9df1b452f4753 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -20,7 +20,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license'; @@ -122,18 +121,6 @@ export const DatavisualizerSelector: FC = () => { values={{ maxFileSize }} /> } - betaBadgeLabel={i18n.translate( - 'xpack.ml.datavisualizer.selector.experimentalBadgeLabel', - { - defaultMessage: 'Experimental', - } - )} - betaBadgeTooltipContent={ - - } footer={ { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { - services: { docLinks, dataVisualizer }, + services: { + docLinks, + dataVisualizer, + data: { + indexPatterns: { get: getIndexPattern }, + }, + }, } = useMlKibana(); - const [FileDataVisualizer, setFileDataVisualizer] = useState | null>(null); + const mlUrlGenerator = useMlUrlGenerator(); + getMlNodeCount(); + + const [FileDataVisualizer, setFileDataVisualizer] = useState(null); + + const links: ResultLink[] = useMemo( + () => [ + { + id: 'create_ml_job', + title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.anomalyDetectionTitle', { + defaultMessage: 'Create new ML job', + }), + description: '', + icon: 'machineLearningApp', + type: 'file', + getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + return await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: indexPatternId, + globalState, + }, + }); + }, + canDisplay: async ({ indexPatternId }) => { + try { + const { timeFieldName } = await getIndexPattern(indexPatternId); + return ( + isFullLicense() && + timeFieldName !== undefined && + checkPermission('canCreateJob') && + mlNodesAvailable() + ); + } catch (error) { + return false; + } + }, + }, + { + id: 'open_in_data_viz', + title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.dataframeTitle', { + defaultMessage: 'Open in Data Visualizer', + }), + description: '', + icon: 'dataVisualizer', + type: 'file', + getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + return await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: indexPatternId, + globalState, + }, + }); + }, + canDisplay: async () => true, + }, + ], + [] + ); useEffect(() => { if (dataVisualizer !== undefined) { + getMlNodeCount(); const { getFileDataVisualizerComponent } = dataVisualizer; getFileDataVisualizerComponent().then(setFileDataVisualizer); } @@ -29,7 +106,7 @@ export const FileDataVisualizerPage: FC = () => { return ( - {FileDataVisualizer} + {FileDataVisualizer !== null && } ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index af803d32e51393..60b6c90370b61d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -5,20 +5,40 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; -import { useMlKibana, useTimefilter } from '../../contexts/kibana'; +import React, { FC, Fragment, useEffect, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana, useTimefilter, useMlUrlGenerator } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { HelpMenu } from '../../components/help_menu'; -import type { IndexDataVisualizerViewProps } from '../../../../../data_visualizer/public'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { isFullLicense } from '../../license'; +import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; +import { checkPermission } from '../../capabilities/check_capabilities'; + +import type { ResultLink, IndexDataVisualizerSpec } from '../../../../../data_visualizer/public'; + +interface GetUrlParams { + indexPatternId: string; + globalState: any; +} + export const IndexDataVisualizerPage: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { - services: { docLinks, dataVisualizer }, + services: { + docLinks, + dataVisualizer, + data: { + indexPatterns: { get: getIndexPattern }, + }, + }, } = useMlKibana(); - const [ - IndexDataVisualizer, - setIndexDataVisualizer, - ] = useState | null>(null); + const mlUrlGenerator = useMlUrlGenerator(); + getMlNodeCount(); + + const [IndexDataVisualizer, setIndexDataVisualizer] = useState( + null + ); useEffect(() => { if (dataVisualizer !== undefined) { @@ -26,10 +46,84 @@ export const IndexDataVisualizerPage: FC = () => { getIndexDataVisualizerComponent().then(setIndexDataVisualizer); } }, []); + + const links: ResultLink[] = useMemo( + () => [ + { + id: 'create_ml_ad_job', + title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionTitle', { + defaultMessage: 'Advanced anomaly detection', + }), + description: i18n.translate( + 'xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionDescription', + { + defaultMessage: + 'Create a job with the full range of options for more advanced use cases.', + } + ), + icon: 'createAdvancedJob', + type: 'file', + getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + return await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED, + pageState: { + index: indexPatternId, + globalState, + }, + }); + }, + canDisplay: async ({ indexPatternId }) => { + try { + const { timeFieldName } = await getIndexPattern(indexPatternId); + return ( + isFullLicense() && + timeFieldName !== undefined && + checkPermission('canCreateJob') && + mlNodesAvailable() + ); + } catch (error) { + return false; + } + }, + dataTestSubj: 'dataVisualizerCreateAdvancedJobCard', + }, + { + id: 'create_ml_dfa_job', + title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.dataframeTitle', { + defaultMessage: 'Data frame analytics', + }), + description: i18n.translate( + 'xpack.ml.indexDatavisualizer.actionsPanel.dataframeDescription', + { + defaultMessage: 'Create outlier detection, regression, or classification analytics.', + } + ), + icon: 'classificationJob', + type: 'file', + getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + return await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB, + pageState: { + index: indexPatternId, + globalState, + }, + }); + }, + canDisplay: async () => { + return ( + isFullLicense() && checkPermission('canCreateDataFrameAnalytics') && mlNodesAvailable() + ); + }, + dataTestSubj: 'dataVisualizerCreateDataFrameAnalyticsCard', + }, + ], + [] + ); + return IndexDataVisualizer ? ( - {IndexDataVisualizer} + {IndexDataVisualizer !== null && } ) : ( diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index d959328218a187..82f8a90fafb7df 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -40,7 +40,6 @@ import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; import { FormattedTooltip, MlTooltipComponent } from '../components/chart_tooltip/chart_tooltip'; import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; -import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; @@ -62,6 +61,9 @@ declare global { } } +function getFormattedSeverityScore(score: number): string { + return String(parseInt(String(score), 10)); +} /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ @@ -122,7 +124,7 @@ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { defaultMessage: 'Max anomaly score', }), - value: cell.formattedValue, + value: cell.formattedValue === '0' ? ' < 1' : cell.formattedValue, color: cell.color, // @ts-ignore seriesIdentifier: { @@ -408,73 +410,75 @@ export const SwimlaneContainer: FC = ({ grow={false} > <> -
- {showSwimlane && !isLoading && ( - - - - - - )} +
+
+ {showSwimlane && !isLoading && ( + + - {isLoading && ( - - + + )} + + {isLoading && ( + + + + )} + {!isLoading && !showSwimlane && ( + {noDataWarning}} /> - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} - /> - )} + )} +
{swimlaneType === SWIMLANE_TYPE.OVERALL && showSwimlane && diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index c9316dcd45ef77..793bf3a4f8ef9f 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -26,6 +26,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` isGlobalCalendar={false} isNewCalendarIdValid={true} jobIds={Array []} + loading={true} onCalendarIdChange={[Function]} onCreate={[Function]} onCreateGroupOption={[Function]} diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index 49caddfd29f825..410017795c6cf5 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -192,6 +192,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` canDeleteCalendar={true} eventsList={Array []} onDeleteClick={[MockFunction]} + saving={false} showImportModal={[MockFunction]} showNewEventModal={[MockFunction]} showSearchBar={true} diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js index da131e2e998fa6..1792f5324396a9 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js @@ -69,6 +69,7 @@ export const CalendarForm = ({ showImportModal, onJobSelection, saving, + loading, selectedGroupOptions, selectedJobOptions, showNewEventModal, @@ -83,7 +84,11 @@ export const CalendarForm = ({ const helpText = isNewCalendarIdValid === true && !isEdit ? msg : undefined; const error = isNewCalendarIdValid === false && !isEdit ? [msg] : undefined; const saveButtonDisabled = - canCreateCalendar === false || saving || !isNewCalendarIdValid || calendarId === ''; + canCreateCalendar === false || + saving || + !isNewCalendarIdValid || + calendarId === '' || + loading === true; const redirectToCalendarsManagementPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE); return ( @@ -116,7 +121,7 @@ export const CalendarForm = ({ name="calendarId" value={calendarId} onChange={onCalendarIdChange} - disabled={isEdit === true || saving === true} + disabled={isEdit === true || saving === true || loading === true} data-test-subj="mlCalendarIdInput" /> @@ -133,7 +138,7 @@ export const CalendarForm = ({ name="description" value={description} onChange={onDescriptionChange} - disabled={isEdit === true || saving === true} + disabled={isEdit === true || saving === true || loading === true} data-test-subj="mlCalendarDescriptionInput" /> @@ -152,7 +157,7 @@ export const CalendarForm = ({ } checked={isGlobalCalendar} onChange={onGlobalCalendarChange} - disabled={saving === true || canCreateCalendar === false} + disabled={saving === true || canCreateCalendar === false || loading === true} data-test-subj="mlCalendarApplyToAllJobsSwitch" /> @@ -172,7 +177,7 @@ export const CalendarForm = ({ options={jobIds} selectedOptions={selectedJobOptions} onChange={onJobSelection} - isDisabled={saving === true || canCreateCalendar === false} + isDisabled={saving === true || canCreateCalendar === false || loading === true} data-test-subj="mlCalendarJobSelection" /> @@ -190,7 +195,7 @@ export const CalendarForm = ({ options={groupIds} selectedOptions={selectedGroupOptions} onChange={onGroupSelection} - isDisabled={saving === true || canCreateCalendar === false} + isDisabled={saving === true || canCreateCalendar === false || loading === true} data-test-subj="mlCalendarJobGroupSelection" /> @@ -215,6 +220,8 @@ export const CalendarForm = ({ onDeleteClick={onEventDelete} showImportModal={showImportModal} showNewEventModal={showNewEventModal} + loading={loading} + saving={saving} showSearchBar /> @@ -272,6 +279,7 @@ CalendarForm.propTypes = { showImportModal: PropTypes.func.isRequired, onJobSelection: PropTypes.func.isRequired, saving: PropTypes.bool.isRequired, + loading: PropTypes.bool.isRequired, selectedGroupOptions: PropTypes.array.isRequired, selectedJobOptions: PropTypes.array.isRequired, showNewEventModal: PropTypes.func.isRequired, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index 4f5364136f9234..ad5de1f92dae5c 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -15,14 +15,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; -function DeleteButton({ onClick, canDeleteCalendar, testSubj }) { +function DeleteButton({ onClick, testSubj, disabled }) { return ( { const sorting = { sort: { @@ -93,7 +95,7 @@ export const EventsTable = ({ render: (event) => ( { onDeleteClick(event.event_id); }} @@ -105,7 +107,7 @@ export const EventsTable = ({ const search = { toolsRight: [ , { expect(wrapper).toMatchSnapshot(); }); - test('Import modal shown on Import Events button click', () => { + test('Import modal button is disabled', () => { const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-test-subj="mlCalendarImportEventsButton"]'); const button = importButton.find('EuiButton'); - button.simulate('click'); - - expect(wrapper.state('isImportModalVisible')).toBe(true); + expect(button.prop('isDisabled')).toBe(true); }); - test('New event modal shown on New event button click', () => { + test('New event modal button is disabled', () => { const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-test-subj="mlCalendarNewEventButton"]'); const button = importButton.find('EuiButton'); button.simulate('click'); - expect(wrapper.state('isNewEventModalVisible')).toBe(true); + expect(button.prop('isDisabled')).toBe(true); }); test('isDuplicateId returns true if form calendar id already exists in calendars', () => { diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index d1e7154fb0dc0a..abdea2df3b2b54 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -58,7 +58,7 @@ export function toggleOsqueryPlugin( registerExtension({ package: OSQUERY_INTEGRATION_NAME, view: 'package-detail-custom', - component: LazyOsqueryManagedCustomButtonExtension, + Component: LazyOsqueryManagedCustomButtonExtension, }); } @@ -146,13 +146,13 @@ export class OsqueryPlugin implements Plugin DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; + +export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok'; + +export interface IlmPolicyStatusResponse { + status: IlmPolicyMigrationStatus; +} diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index ddba61e9a0b8db..6a8f3a3e4e5ecd 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -6,6 +6,7 @@ "configPath": ["xpack", "reporting"], "requiredPlugins": [ "data", + "esUiShared", "home", "management", "licensing", diff --git a/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx b/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx new file mode 100644 index 00000000000000..78b2e77d09aeea --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/ilm_policy_status_context.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React, { createContext, useContext } from 'react'; + +import { IlmPolicyStatusResponse } from '../../common/types'; + +import { useCheckIlmPolicyStatus } from './reporting_api_client'; + +type UseCheckIlmPolicyStatus = ReturnType; + +interface ContextValue { + status: undefined | IlmPolicyStatusResponse['status']; + isLoading: UseCheckIlmPolicyStatus['isLoading']; + recheckStatus: UseCheckIlmPolicyStatus['resendRequest']; +} + +const IlmPolicyStatusContext = createContext(undefined); + +export const IlmPolicyStatusContextProvider: FunctionComponent = ({ children }) => { + const { isLoading, data, resendRequest: recheckStatus } = useCheckIlmPolicyStatus(); + + return ( + + {children} + + ); +}; + +export type UseIlmPolicyStatusReturn = ReturnType; + +export const useIlmPolicyStatus = (): ContextValue => { + const ctx = useContext(IlmPolicyStatusContext); + if (!ctx) { + throw new Error('"useIlmPolicyStatus" can only be used inside of "IlmPolicyStatusContext"'); + } + return ctx; +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx new file mode 100644 index 00000000000000..37857943774d41 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from 'src/core/public'; +import type { FunctionComponent } from 'react'; +import React, { createContext, useContext } from 'react'; + +import type { ReportingAPIClient } from './reporting_api_client'; + +interface ContextValue { + http: HttpSetup; + apiClient: ReportingAPIClient; +} + +const InternalApiClientContext = createContext(undefined); + +export const InternalApiClientClientProvider: FunctionComponent<{ + http: HttpSetup; + apiClient: ReportingAPIClient; +}> = ({ http, apiClient, children }) => { + return ( + + {children} + + ); +}; + +export const useInternalApiClient = (): ContextValue => { + const ctx = useContext(InternalApiClientContext); + if (!ctx) { + throw new Error('"useInternalApiClient" can only be used inside of "InternalApiClientContext"'); + } + return ctx; +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts new file mode 100644 index 00000000000000..afd8222fd3831e --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRequest, UseRequestResponse } from '../../shared_imports'; +import { IlmPolicyStatusResponse } from '../../../common/types'; + +import { API_GET_ILM_POLICY_STATUS } from '../../../common/constants'; + +import { useInternalApiClient } from './context'; + +export const useCheckIlmPolicyStatus = (): UseRequestResponse => { + const { http } = useInternalApiClient(); + return useRequest(http, { path: API_GET_ILM_POLICY_STATUS, method: 'get' }); +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts new file mode 100644 index 00000000000000..b32d675a1d209c --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './reporting_api_client'; + +export * from './hooks'; + +export { InternalApiClientClientProvider, useInternalApiClient } from './context'; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts similarity index 93% rename from x-pack/plugins/reporting/public/lib/reporting_api_client.ts rename to x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 4ce9e8760f21fa..64caac0e27bddc 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -12,8 +12,9 @@ import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, + API_MIGRATE_ILM_POLICY_URL, REPORTING_MANAGEMENT_HOME, -} from '../../common/constants'; +} from '../../../common/constants'; import { DownloadReportFn, JobId, @@ -21,8 +22,8 @@ import { ReportApiJSON, ReportDocument, ReportSource, -} from '../../common/types'; -import { add } from '../notifier/job_completion_notifications'; +} from '../../../common/types'; +import { add } from '../../notifier/job_completion_notifications'; export interface JobQueueEntry { _id: string; @@ -167,4 +168,8 @@ export class ReportingAPIClient { this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { asSystemRequest: true, }); + + public migrateReportingIndicesIlmPolicy = (): Promise => { + return this.http.put(`${API_MIGRATE_ILM_POLICY_URL}`); + }; } diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 93df3c8d999357..9ce249aa32a1dc 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -1,82 +1,116 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReportListing Report job listing with some items 1`] = ` -Array [ - -
-
- +
+
+ +
+
- +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + + + + + + + + + -
-
-
- -
- -
- - - - -
+ +
- - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - -
+ + - -
+ + -
+ - -
- -
-
- + Report + -
-
- - - - - - Report - - - - - - + + + + + - - + - - - - Created at - - - - - - + Created at + + + + + + + + - - + - - - - Status - - - - - - + Status + + + + + + + + - - - - - - Actions - - - - - -
+ -
- Loading reports + Actions -
-
-
-
-
- , -
-
- -
+ + + + + + + + + - -
+ - -
- - -
- -
- -
- - -
- - -
- -
- -
- - - - - - - - - - - - - + + - - - + canvas workpad + + + + + + + + + - - - + Created at + +
+
+
+ 2020-04-14 @ 05:01 PM +
+ + elastic + +
+
+ + + -
- - - + - - - - - - - - + + + + + + + + - - - - - -
- -
-
- - - - + Report + +
+
+
+ My Canvas Workpad +
+ +
- - Report - - - - - -
- - - - - - Created at - - - - - - - - - - - - Status - - - - - - - - - - + Status + +
+
+ + Pending - waiting for job to be processed + +
+
+ + + +
+
+ + +
- - Actions - - - - - - - + + + + + + + + + +
+
+ + + +
+ +
+
+ + +
+ +
+
+ + +
+
+
+ Report +
+
- + My Canvas Workpad +
+ - Loading reports +
+ + + canvas workpad + + +
+
+
+ +
+
+ Created at +
+
+
+
+ 2020-04-14 @ 05:01 PM +
+ + elastic
-
-
+
+ + + + +
+ Status +
+
+
+ + 2020-04-14 @ 05:01 PM + , + } + } + > + Processing (attempt 1 of 1) at + + 2020-04-14 @ 05:01 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 04:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 04:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 04:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:20 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:21 PM + , + } + } + > + Completed with warnings at + + 2020-04-14 @ 01:21 PM + + + +
+ + + + Errors occurred: see job info for details. + + + +
+
+
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:17 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:18 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:18 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + + canvas workpad + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:12 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:13 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:13 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ count +
+ +
+ + + visualization + + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-09 @ 03:09 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-09 @ 03:10 PM + , + } + } + > + Completed at + + 2020-04-09 @ 03:10 PM + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + +
+ + } + onPageChange={[Function]} + onPageSizeChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": true, + "pageIndex": 0, + "pageSize": 10, + "totalItemCount": 18, + } + } + > +
+ +
+ + + +
+ +
+ + +
+ + + +
+
+
+ +
-
, -] + +
`; diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx new file mode 100644 index 00000000000000..3945ec5be9fa7e --- /dev/null +++ b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; +import type { ApplicationStart } from 'src/core/public'; + +import { ILM_POLICY_NAME } from '../../common/constants'; +import { LocatorPublic, SerializableState } from '../shared_imports'; + +interface Props { + navigateToUrl: ApplicationStart['navigateToUrl']; + locator: LocatorPublic; +} + +const i18nTexts = { + buttonLabel: i18n.translate('xpack.reporting.listing.reports.ilmPolicyLinkText', { + defaultMessage: 'Edit ILM policy', + }), +}; + +export const IlmPolicyLink: FunctionComponent = ({ locator, navigateToUrl }) => { + return ( + { + locator + .getUrl({ + page: 'policy_edit', + policyName: ILM_POLICY_NAME, + }) + .then((url) => { + navigateToUrl(url); + }); + }} + > + {i18nTexts.buttonLabel} + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx new file mode 100644 index 00000000000000..5bb3ac524e1306 --- /dev/null +++ b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/ilm_policy_migration_needed_callout.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { FunctionComponent } from 'react'; +import React, { useState } from 'react'; +import { EuiCallOut, EuiButton, EuiCode } from '@elastic/eui'; + +import type { NotificationsSetup } from 'src/core/public'; + +import { ILM_POLICY_NAME } from '../../../common/constants'; + +import { useInternalApiClient } from '../../lib/reporting_api_client'; + +const i18nTexts = { + title: i18n.translate('xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle', { + defaultMessage: 'Migrate reporting indices', + }), + description: ( + {ILM_POLICY_NAME}, + }} + /> + ), + buttonLabel: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesButtonLabel', + { + defaultMessage: 'Migrate indices', + } + ), + migrateErrorTitle: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesErrorTitle', + { + defaultMessage: 'Could not migrate reporting indices', + } + ), + migrateSuccessTitle: i18n.translate( + 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesSuccessTitle', + { + defaultMessage: 'Successfully migrated reporting indices', + } + ), +}; + +interface Props { + toasts: NotificationsSetup['toasts']; + onMigrationDone: () => void; +} + +export const IlmPolicyMigrationNeededCallOut: FunctionComponent = ({ + toasts, + onMigrationDone, +}) => { + const [isMigratingIndices, setIsMigratingIndices] = useState(false); + + const { apiClient } = useInternalApiClient(); + + const migrateReportingIndices = async () => { + try { + setIsMigratingIndices(true); + await apiClient.migrateReportingIndicesIlmPolicy(); + onMigrationDone(); + toasts.addSuccess({ title: i18nTexts.migrateSuccessTitle }); + } catch (e) { + toasts.addError(e, { + title: i18nTexts.migrateErrorTitle, + toastMessage: e.body?.message, + }); + } finally { + setIsMigratingIndices(false); + } + }; + + return ( + +

{i18nTexts.description}

+ + {i18nTexts.buttonLabel} + +
+ ); +}; diff --git a/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.tsx new file mode 100644 index 00000000000000..892cbcdde5edee --- /dev/null +++ b/x-pack/plugins/reporting/public/management/migrate_ilm_policy_callout/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React from 'react'; +import { EuiSpacer, EuiFlexItem } from '@elastic/eui'; + +import { NotificationsSetup } from 'src/core/public'; + +import { useIlmPolicyStatus } from '../../lib/ilm_policy_status_context'; + +import { IlmPolicyMigrationNeededCallOut } from './ilm_policy_migration_needed_callout'; + +interface Props { + toasts: NotificationsSetup['toasts']; +} + +export const MigrateIlmPolicyCallOut: FunctionComponent = ({ toasts }) => { + const { isLoading, recheckStatus, status } = useIlmPolicyStatus(); + + if (isLoading || !status || status === 'ok') { + return null; + } + + return ( + <> + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index eb1057a9bdfc77..8d147628c66621 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -10,10 +10,11 @@ import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Observable } from 'rxjs'; import { CoreSetup, CoreStart } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; import { ILicense } from '../../../licensing/public'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { ClientConfigType } from '../plugin'; +import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; import { ReportListing } from './report_listing'; export async function mountManagementSection( @@ -22,17 +23,23 @@ export async function mountManagementSection( license$: Observable, pollConfig: ClientConfigType['poll'], apiClient: ReportingAPIClient, + urlService: SharePluginSetup['url'], params: ManagementAppMountParams ) { render( - + + + + + , params.element ); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index efc1a7dfe3b20a..0b278cbaa0449f 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -7,9 +7,22 @@ import React from 'react'; import { Observable } from 'rxjs'; -import { mountWithIntl } from '@kbn/test/jest'; -import { ILicense } from '../../../licensing/public'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { UnwrapPromise } from '@kbn/utility-types'; + +import { act } from 'react-dom/test-utils'; + +import { registerTestBed } from '@kbn/test/jest'; + +import type { SharePluginSetup, LocatorPublic } from '../../../../../src/plugins/share/public'; +import type { NotificationsSetup } from '../../../../../src/core/public'; +import { httpServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks'; + +import type { ILicense } from '../../../licensing/public'; + +import { IlmPolicyMigrationStatus } from '../../common/types'; + +import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -17,7 +30,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }; }); -import { ReportListing } from './report_listing'; +import { ReportListing, Props } from './report_listing'; const reportingAPIClient = { list: () => @@ -33,6 +46,7 @@ const reportingAPIClient = { { _id: 'k8t4ylcb07mi9d006214ifyg', _index: '.reporting-2020.04.05', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization', }, output: { content_type: 'image/png', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-09T19:09:52.137Z', layout: { dimensions: { height: 1575, width: 1423, }, id: 'png', }, objectType: 'visualization', relativeUrl: "/s/hsyjklk/app/visualize#/edit/94d1fe40-7a94-11ea-b373-0749f92ad295?_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(),schema:metric,type:count)),params:(addLegend:!f,addTooltip:!t,metric:(colorSchema:'Green%20to%20Red',colorsRange:!((from:0,to:10000)),invertColors:!f,labels:(show:!t),metricColorMode:None,percentageMode:!f,style:(bgColor:!f,bgFill:%23000,fontSize:60,labelColor:!f,subText:''),useRanges:!f),type:metric),title:count,type:metric))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&indexPattern=d81752b0-7434-11ea-be36-1f978cda44d4&type=metric", title: 'count', }, priority: 10, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000, }, sort: [1586459392139], }, ]), // prettier-ignore total: () => Promise.resolve(18), + migrateReportingIndicesIlmPolicy: jest.fn(), } as any; const validCheck = { @@ -48,10 +62,6 @@ const license$ = { }, } as Observable; -const toasts = { - addDanger: jest.fn(), -} as any; - const mockPollConfig = { jobCompletionNotifier: { interval: 5000, @@ -64,22 +74,87 @@ const mockPollConfig = { }; describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - ; + let ilmLocator: undefined | LocatorPublic; + let urlService: SharePluginSetup['url']; + let testBed: UnwrapPromise>; + let toasts: NotificationsSetup['toasts']; + + const createTestBed = registerTestBed( + (props?: Partial) => ( + - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); + http={httpService} + > + + + + + ), + { memoryRouter: { wrapComponent: false } } + ); + + const setup = async (props?: Partial) => { + const tb = await createTestBed(props); + const { find, exists, component } = tb; + + return { + ...tb, + actions: { + findListTable: () => find('reportJobListing'), + hasIlmMigrationBanner: () => exists('migrateReportingIndicesPolicyCallOut'), + hasIlmPolicyLink: () => exists('ilmPolicyLink'), + migrateIndices: async () => { + await act(async () => { + find('migrateReportingIndicesButton').simulate('click'); + }); + component.update(); + }, + }, + }; + }; + + const runSetup = async (props?: Partial) => { + await act(async () => { + testBed = await setup(props); + }); + testBed.component.update(); + }; + + beforeEach(async () => { + toasts = notificationServiceMock.createSetupContract().toasts; + httpService = httpServiceMock.createSetupContract(); + ilmLocator = ({ + getUrl: jest.fn(), + } as unknown) as LocatorPublic; + + urlService = ({ + locators: { + get: () => ilmLocator, + }, + } as unknown) as SharePluginSetup['url']; + await runSetup(); + }); + + afterEach(() => { + jest.clearAllMocks(); }); - it('subscribes to license changes, and unsubscribes on dismount', () => { + it('Report job listing with some items', () => { + const { actions } = testBed; + const table = actions.findListTable(); + expect(table).toMatchSnapshot(); + }); + + it('subscribes to license changes, and unsubscribes on dismount', async () => { const unsubscribeMock = jest.fn(); const subMock = { subscribe: jest.fn().mockReturnValue({ @@ -87,19 +162,103 @@ describe('ReportListing', () => { }), } as any; - const wrapper = mountWithIntl( - } - pollConfig={mockPollConfig} - redirect={jest.fn()} - toasts={toasts} - /> - ); - wrapper.update(); + await runSetup({ license$: subMock }); + expect(subMock.subscribe).toHaveBeenCalled(); expect(unsubscribeMock).not.toHaveBeenCalled(); - wrapper.unmount(); + testBed.component.unmount(); expect(unsubscribeMock).toHaveBeenCalled(); }); + + describe('ILM policy', () => { + beforeEach(async () => { + httpService = httpServiceMock.createSetupContract(); + ilmLocator = ({ + getUrl: jest.fn(), + } as unknown) as LocatorPublic; + + urlService = ({ + locators: { + get: () => ilmLocator, + }, + } as unknown) as SharePluginSetup['url']; + + await runSetup(); + }); + + it('shows the migrate banner when migration status is not "OK"', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmMigrationBanner()).toBe(true); + }); + + it('does not show the migrate banner when migration status is "OK"', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmMigrationBanner()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy', async () => { + const status: IlmPolicyMigrationStatus = 'policy-not-found'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('hides the ILM policy link if there is no ILM policy locator', async () => { + ilmLocator = undefined; + const status: IlmPolicyMigrationStatus = 'ok'; // should never happen, but need to test that when the locator is missing we don't render the link + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(false); + }); + + it('always shows the ILM policy link if there is an ILM policy', async () => { + const status: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValue({ status }); + await runSetup(); + const { actions } = testBed; + expect(actions.hasIlmPolicyLink()).toBe(true); + + const status2: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValue({ status: status2 }); + await runSetup(); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + + it('hides the banner after migrating indices', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + const status2: IlmPolicyMigrationStatus = 'ok'; + httpService.get.mockResolvedValueOnce({ status }); + httpService.get.mockResolvedValueOnce({ status: status2 }); + await runSetup(); + const { actions } = testBed; + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(actions.hasIlmMigrationBanner()).toBe(false); + expect(actions.hasIlmPolicyLink()).toBe(true); + expect(toasts.addSuccess).toHaveBeenCalledTimes(1); + }); + + it('informs users when migrations failed', async () => { + const status: IlmPolicyMigrationStatus = 'indices-not-managed-by-policy'; + httpService.get.mockResolvedValueOnce({ status }); + reportingAPIClient.migrateReportingIndicesIlmPolicy.mockRejectedValueOnce(new Error('oops!')); + await runSetup(); + const { actions } = testBed; + + expect(actions.hasIlmMigrationBanner()).toBe(true); + await actions.migrateIndices(); + expect(toasts.addError).toHaveBeenCalledTimes(1); + expect(actions.hasIlmMigrationBanner()).toBe(true); + expect(actions.hasIlmPolicyLink()).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 0b6ece4d8bd021..749e42de526d32 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTextColor, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -26,10 +27,18 @@ import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; import { checkLicense } from '../lib/license_check'; -import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { + JobQueueEntry, + ReportingAPIClient, + useInternalApiClient, +} from '../lib/reporting_api_client'; +import { useIlmPolicyStatus, UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import type { SharePluginSetup } from '../shared_imports'; import { ClientConfigType } from '../plugin'; import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; import { ReportDiagnostic } from './report_diagnostic'; +import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import { IlmPolicyLink } from './ilm_policy_link'; export interface Job { id: string; @@ -55,7 +64,10 @@ export interface Props { license$: LicensingPluginSetup['license$']; pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; toasts: ToastsSetup; + urlService: SharePluginSetup['url']; + ilmPolicyContextValue: UseIlmPolicyStatusReturn; } interface State { @@ -132,6 +144,10 @@ class ReportListingUi extends Component { } public render() { + const { ilmPolicyContextValue, urlService, navigateToUrl } = this.props; + const ilmLocator = urlService.locators.get('ILM_LOCATOR_ID'); + const hasIlmPolicy = ilmPolicyContextValue.status !== 'policy-not-found'; + const showIlmPolicyLink = Boolean(ilmLocator && hasIlmPolicy); return ( <> { } /> + + {this.renderTable()} - + + + {ilmPolicyContextValue.isLoading ? ( + + ) : ( + showIlmPolicyLink && ( + + ) + )} + @@ -531,4 +558,18 @@ class ReportListingUi extends Component { } } -export const ReportListing = injectI18n(ReportListingUi); +const PrivateReportListing = injectI18n(ReportListingUi); + +export const ReportListing = ( + props: Omit +) => { + const ilmPolicyStatusValue = useIlmPolicyStatus(); + const { apiClient } = useInternalApiClient(); + return ( + + ); +}; diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index a2881af902072e..fcbc4662c6e593 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -15,7 +15,6 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; -import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, @@ -23,7 +22,6 @@ import { HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; -import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { constants, getDefaultLayoutSelectors } from '../common'; import { durationToNumber } from '../common/schema_utils'; @@ -37,6 +35,13 @@ import { getSharedComponents } from './shared'; import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; +import type { + SharePluginSetup, + SharePluginStart, + UiActionsSetup, + UiActionsStart, +} from './shared_imports'; + export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; roles: { enabled: boolean }; @@ -159,6 +164,7 @@ export class ReportingPublicPlugin license$, this.config.poll, apiClient, + share.url, params ); }, diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts new file mode 100644 index 00000000000000..010da46c074016 --- /dev/null +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + SharePluginSetup, + SharePluginStart, + LocatorPublic, +} from '../../../../src/plugins/share/public'; + +export { useRequest, UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; + +export type { SerializableState } from 'src/plugins/kibana_utils/common'; + +export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; + +export type { ManagementAppMountParams } from 'src/plugins/management/public'; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts new file mode 100644 index 00000000000000..dc20f92f38c94d --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/check_ilm_migration_status.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + IndicesIndexStatePrefixedSettings, + IndicesIndexSettings, +} from '@elastic/elasticsearch/api/types'; +import { ILM_POLICY_NAME } from '../../../common/constants'; +import { IlmPolicyMigrationStatus } from '../../../common/types'; +import { IlmPolicyManager } from '../../lib/store/ilm_policy_manager'; +import type { DeprecationsDependencies } from './types'; + +export const checkIlmMigrationStatus = async ({ + reportingCore, + elasticsearchClient, +}: DeprecationsDependencies): Promise => { + const ilmPolicyManager = IlmPolicyManager.create({ client: elasticsearchClient }); + if (!(await ilmPolicyManager.doesIlmPolicyExist())) { + return 'policy-not-found'; + } + + const store = await reportingCore.getStore(); + const indexPattern = store.getReportingIndexPattern(); + + const { body: reportingIndicesSettings } = await elasticsearchClient.indices.getSettings({ + index: indexPattern, + }); + + const hasUnmanagedIndices = Object.values(reportingIndicesSettings).some((settings) => { + return ( + (settings?.settings as IndicesIndexStatePrefixedSettings)?.index?.lifecycle?.name !== + ILM_POLICY_NAME && + (settings?.settings as IndicesIndexSettings)?.['index.lifecycle']?.name !== ILM_POLICY_NAME + ); + }); + + return hasUnmanagedIndices ? 'indices-not-managed-by-policy' : 'ok'; +}; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/index.ts b/x-pack/plugins/reporting/server/lib/deprecations/index.ts new file mode 100644 index 00000000000000..95594940e07e23 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { checkIlmMigrationStatus } from './check_ilm_migration_status'; + +export const deprecations = { + checkIlmMigrationStatus, +}; diff --git a/x-pack/plugins/reporting/server/lib/deprecations/types.ts b/x-pack/plugins/reporting/server/lib/deprecations/types.ts new file mode 100644 index 00000000000000..c6e9e3b7ad920a --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/deprecations/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; +import type { ReportingCore } from '../../core'; + +export interface DeprecationsDependencies { + reportingCore: ReportingCore; + elasticsearchClient: ElasticsearchClient; +} diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index e66f72f88f8ea7..b2a2a1edcd6a58 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -10,5 +10,5 @@ export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; export { LevelLogger } from './level_logger'; export { statuses } from './statuses'; -export { ReportingStore } from './store'; +export { ReportingStore, IlmPolicyManager } from './store'; export { startTrace } from './trace'; diff --git a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts similarity index 83% rename from x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts rename to x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts index 90636e3c523a32..bea2ba21c0846f 100644 --- a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IlmPutLifecycleRequest } from '@elastic/elasticsearch/api/types'; +import type { IlmPutLifecycleRequest } from '@elastic/elasticsearch/api/types'; export const reportingIlmPolicy: IlmPutLifecycleRequest['body'] = { policy: { diff --git a/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts new file mode 100644 index 00000000000000..ca0a74cae87266 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/ilm_policy_manager.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; +import { ILM_POLICY_NAME } from '../../../../common/constants'; + +import { reportingIlmPolicy } from './constants'; + +/** + * Responsible for detecting and provisioning the reporting ILM policy. + * + * Uses the provided {@link ElasticsearchClient} to scope request privileges. + */ +export class IlmPolicyManager { + constructor(private readonly client: ElasticsearchClient) {} + + public static create(opts: { client: ElasticsearchClient }) { + return new IlmPolicyManager(opts.client); + } + + public async doesIlmPolicyExist(): Promise { + try { + await this.client.ilm.getLifecycle({ policy: ILM_POLICY_NAME }); + return true; + } catch (e) { + if (e.statusCode === 404) { + return false; + } + throw e; + } + } + + /** + * Create the Reporting ILM policy + */ + public async createIlmPolicy(): Promise { + await this.client.ilm.putLifecycle({ + policy: ILM_POLICY_NAME, + body: reportingIlmPolicy, + }); + } +} diff --git a/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts new file mode 100644 index 00000000000000..045a9ecb59997d --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/ilm_policy_manager/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { reportingIlmPolicy } from './constants'; +export { IlmPolicyManager } from './ilm_policy_manager'; diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index 6b979325921a69..888918abbc344a 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -8,3 +8,4 @@ export { ReportDocument } from '../../../common/types'; export { Report } from './report'; export { ReportingStore } from './store'; +export { IlmPolicyManager } from './ilm_policy_manager'; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 17c067a255b38d..7a7dd20e1b25cf 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -10,12 +10,15 @@ import { ElasticsearchClient } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; import { JobStatus } from '../../../common/types'; + +import { ILM_POLICY_NAME } from '../../../common/constants'; + import { ReportTaskParams } from '../tasks'; + +import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; -import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report'; - -import { reportingIlmPolicy } from './report_ilm_policy'; +import { IlmPolicyManager } from './ilm_policy_manager'; /* * When an instance of Kibana claims a report job, this information tells us about that instance @@ -92,6 +95,7 @@ export class ReportingStore { private readonly indexPrefix: string; // config setting of index prefix in system index name private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work private client?: ElasticsearchClient; + private ilmPolicyManager?: IlmPolicyManager; constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { const config = reportingCore.getConfig(); @@ -109,6 +113,15 @@ export class ReportingStore { return this.client; } + private async getIlmPolicyManager() { + if (!this.ilmPolicyManager) { + const client = await this.getClient(); + this.ilmPolicyManager = IlmPolicyManager.create({ client }); + } + + return this.ilmPolicyManager; + } + private async createIndex(indexName: string) { const client = await this.getClient(); const { body: exists } = await client.indices.exists({ index: indexName }); @@ -125,7 +138,7 @@ export class ReportingStore { number_of_shards: 1, auto_expand_replicas: '0-1', lifecycle: { - name: this.ilmPolicyName, + name: ILM_POLICY_NAME, }, }, mappings: { @@ -181,37 +194,19 @@ export class ReportingStore { return client.indices.refresh({ index }); } - private readonly ilmPolicyName = 'kibana-reporting'; - - private async doesIlmPolicyExist(): Promise { - const client = await this.getClient(); - try { - await client.ilm.getLifecycle({ policy: this.ilmPolicyName }); - return true; - } catch (e) { - if (e.statusCode === 404) { - return false; - } - throw e; - } - } - /** * Function to be called during plugin start phase. This ensures the environment is correctly * configured for storage of reports. */ public async start() { - const client = await this.getClient(); + const ilmPolicyManager = await this.getIlmPolicyManager(); try { - if (await this.doesIlmPolicyExist()) { - this.logger.debug(`Found ILM policy ${this.ilmPolicyName}; skipping creation.`); + if (await ilmPolicyManager.doesIlmPolicyExist()) { + this.logger.debug(`Found ILM policy ${ILM_POLICY_NAME}; skipping creation.`); return; } - this.logger.info(`Creating ILM policy for managing reporting indices: ${this.ilmPolicyName}`); - await client.ilm.putLifecycle({ - policy: this.ilmPolicyName, - body: reportingIlmPolicy, - }); + this.logger.info(`Creating ILM policy for managing reporting indices: ${ILM_POLICY_NAME}`); + await ilmPolicyManager.createIlmPolicy(); } catch (e) { this.logger.error('Error in start phase'); this.logger.error(e.body.error); @@ -446,4 +441,8 @@ export class ReportingStore { return body.hits?.hits[0] as ReportRecordTimeout; } + + public getReportingIndexPattern(): string { + return `${this.indexPrefix}-*`; + } } diff --git a/x-pack/plugins/reporting/server/routes/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations.ts new file mode 100644 index 00000000000000..7a38faf60f6bbe --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/deprecations.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { errors } from '@elastic/elasticsearch'; +import { + API_MIGRATE_ILM_POLICY_URL, + API_GET_ILM_POLICY_STATUS, + ILM_POLICY_NAME, +} from '../../common/constants'; +import { IlmPolicyStatusResponse } from '../../common/types'; +import { deprecations } from '../lib/deprecations'; +import { ReportingCore } from '../core'; +import { IlmPolicyManager, LevelLogger as Logger } from '../lib'; + +export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { + const { router } = reporting.getPluginSetupDeps(); + + router.get( + { + path: API_GET_ILM_POLICY_STATUS, + validate: false, + }, + async ( + { + core: { + elasticsearch: { client: scopedClient }, + }, + }, + req, + res + ) => { + const checkIlmMigrationStatus = () => { + return deprecations.checkIlmMigrationStatus({ + reportingCore: reporting, + // We want to make the current status visible to all reporting users + elasticsearchClient: scopedClient.asInternalUser, + }); + }; + + try { + const response: IlmPolicyStatusResponse = { + status: await checkIlmMigrationStatus(), + }; + return res.ok({ body: response }); + } catch (e) { + return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message } }); + } + } + ); + + router.put( + { path: API_MIGRATE_ILM_POLICY_URL, validate: false }, + async ({ core: { elasticsearch } }, req, res) => { + const store = await reporting.getStore(); + const { + client: { asCurrentUser: client }, + } = elasticsearch; + + const scopedIlmPolicyManager = IlmPolicyManager.create({ + client, + }); + + // First we ensure that the reporting ILM policy exists in the cluster + try { + // We don't want to overwrite an existing reporting policy because it may contain alterations made by users + if (!(await scopedIlmPolicyManager.doesIlmPolicyExist())) { + await scopedIlmPolicyManager.createIlmPolicy(); + } + } catch (e) { + return res.customError({ statusCode: e?.statusCode ?? 500, body: { message: e.message } }); + } + + const indexPattern = store.getReportingIndexPattern(); + + // Second we migrate all of the existing indices to be managed by the reporting ILM policy + try { + await client.indices.putSettings({ + index: indexPattern, + body: { + 'index.lifecycle': { + name: ILM_POLICY_NAME, + }, + }, + }); + return res.ok(); + } catch (err) { + logger.error(err); + + if (err instanceof errors.ResponseError) { + // If there were no reporting indices to update, that's OK because then there is nothing to migrate + if (err.statusCode === 404) { + return res.ok(); + } + return res.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message, + name: err.name, + }, + }); + } + + throw err; + } + } + ); +}; diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index e061bd4f7d66c8..a462da38490839 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -6,15 +6,17 @@ */ import { LevelLogger as Logger } from '../lib'; +import { registerDeprecationsRoutes } from './deprecations'; +import { registerDiagnosticRoutes } from './diagnostic'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; import { ReportingCore } from '../core'; -import { registerDiagnosticRoutes } from './diagnostic'; export function registerRoutes(reporting: ReportingCore, logger: Logger) { + registerDeprecationsRoutes(reporting, logger); + registerDiagnosticRoutes(reporting, logger); registerJobGenerationRoutes(reporting, logger); registerJobInfoRoutes(reporting); - registerDiagnosticRoutes(reporting, logger); } export interface ReportingRequestPre { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 3fb32856a1ef11..abfcb4014a79fd 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -78,13 +78,6 @@ export enum SecurityPageName { eventFilters = 'event_filters', } -export enum SecurityPageGroupName { - detect = 'detect', - explore = 'explore', - investigate = 'investigate', - manage = 'manage', -} - export const TIMELINES_PATH = '/timelines'; export const CASES_PATH = '/cases'; export const OVERVIEW_PATH = '/overview'; diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index f4cddfe4d8da95..0713716a15d510 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -174,7 +174,7 @@ Represents all the URLs used during the tests execution. The data the tests need: - Is generated on the fly using our application APIs (preferred way) -- Is ingested on the ELS instance using the `es_archive` utility +- Is ingested on the ELS instance using the `es_archiver` utility By default, when running the tests in Jenkins mode, a base set of data is ingested on the ELS instance: an empty kibana index and a set of auditbeat data (the `empty_kibana` and `auditbeat` archives, respectively). This is usually enough to cover most of the scenarios that we are testing. @@ -200,6 +200,14 @@ node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" - Note that the command will create the folder if it does not exist. +### Using an archive from within the Cypress tests + +Task [cypress/tasks/es_archiver.ts](https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts) provides helpers such as `esArchiverLoad` and `esArchiverUnload` by means of `es_archiver`'s CLI. + +Because of `cy.exec`, used to invoke `es_archiver`, it's necessary to override its environment with `NODE_TLS_REJECT_UNAUTHORIZED=1`. It indeed would inject `NODE_TLS_REJECT_UNAUTHORIZED=0` and make `es_archive` otherwise abort with the following warning if used over https: + +> Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification. + ## Development Best Practices ### Clean up the state diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts index 28240bd12e4be0..4a729ab5044bac 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/inspect.spec.ts @@ -6,17 +6,20 @@ */ import { INSPECT_HOSTS_BUTTONS_IN_SECURITY, INSPECT_MODAL } from '../../screens/inspect'; +import { HOST_OVERVIEW } from '../../screens/hosts/main'; import { cleanKibana } from '../../tasks/common'; -import { closesModal, openStatsAndTables } from '../../tasks/inspect'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { clickInspectButton, closesModal, openStatsAndTables } from '../../tasks/inspect'; +import { loginAndWaitForHostDetailsPage, loginAndWaitForPage } from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; describe('Inspect', () => { + before(() => { + cleanKibana(); + }); context('Hosts stats and tables', () => { before(() => { - cleanKibana(); loginAndWaitForPage(HOSTS_URL); }); afterEach(() => { @@ -30,4 +33,18 @@ describe('Inspect', () => { }) ); }); + + context('Hosts details', () => { + before(() => { + loginAndWaitForHostDetailsPage(); + }); + afterEach(() => { + closesModal(); + }); + + it(`inspects the host details`, () => { + clickInspectButton(HOST_OVERVIEW); + cy.get(INSPECT_MODAL).should('be.visible'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index 77a1775494e6a1..c3e04aaaf6a1f9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -78,10 +78,22 @@ describe('Row renderers', () => { }); it('Selected renderer can be disabled with one click', () => { + // Ensure these elements are visible before continuing since sometimes it takes a second for the modal to show up + // and it gives the click handlers a bit of time to be initialized as well to reduce chances of flake but you still + // have to use pipe() below as an additional measure. cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).should('exist'); - cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN) - .pipe(($el) => $el.trigger('click')) - .should('not.be.visible'); + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked'); + + // Keep clicking on the disable all button until the first element of all the elements are no longer checked. + // In cases where the click handler is not present on the page just yet, this will cause the button to be clicked + // multiple times until it sees that the click took effect. You could go through the whole list but I just check + // for the first to be unchecked and then assume the click was successful + cy.root() + .pipe(($el) => { + $el.find(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).trigger('click'); + return $el.find(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first(); + }) + .should('not.be.checked'); cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts index 3b1df67bec29ca..cdccb6d75d18ce 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/not_found.spec.ts @@ -71,7 +71,7 @@ describe('Display not found page', () => { cy.get(NOT_FOUND).should('exist'); }); - it('navigates to the trusted applications page with incorrect link', () => { + it('navigates to the event filters page with incorrect link', () => { loginAndWaitForPage(`${EVENT_FILTERS_URL}/randomUrl`); cy.get(NOT_FOUND).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/main.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/main.ts index f8b20393ba1102..95381b06f44e92 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/main.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/main.ts @@ -16,3 +16,5 @@ export const EVENTS_TAB = '[data-test-subj="navigation-events"]'; export const KQL_SEARCH_BAR = '[data-test-subj="queryInput"]'; export const UNCOMMON_PROCESSES_TAB = '[data-test-subj="navigation-uncommonProcesses"]'; + +export const HOST_OVERVIEW = `[data-test-subj="host-overview"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts index a0ee5bda82b01a..94ac8003c0d8bd 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts @@ -12,23 +12,28 @@ const CONFIG_PATH = '../../test/functional/config.js'; const ES_URL = Cypress.env('ELASTICSEARCH_URL'); const KIBANA_URL = Cypress.config().baseUrl; +// Otherwise cy.exec would inject NODE_TLS_REJECT_UNAUTHORIZED=0 and node would abort if used over https +const NODE_TLS_REJECT_UNAUTHORIZED = '1'; + export const esArchiverLoad = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); cy.exec( - `node ../../../scripts/es_archiver load "${path}" --config "${CONFIG_PATH}" --es-url "${ES_URL}" --kibana-url "${KIBANA_URL}"` + `node ../../../scripts/es_archiver load "${path}" --config "${CONFIG_PATH}" --es-url "${ES_URL}" --kibana-url "${KIBANA_URL}"`, + { env: { NODE_TLS_REJECT_UNAUTHORIZED } } ); }; export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); cy.exec( - `node ../../../scripts/es_archiver unload "${path}" --config "${CONFIG_PATH}" --es-url "${ES_URL}" --kibana-url "${KIBANA_URL}"` + `node ../../../scripts/es_archiver unload "${path}" --config "${CONFIG_PATH}" --es-url "${ES_URL}" --kibana-url "${KIBANA_URL}"`, + { env: { NODE_TLS_REJECT_UNAUTHORIZED } } ); }; export const esArchiverResetKibana = () => { cy.exec( `node ../../../scripts/es_archiver empty-kibana-index --config "${CONFIG_PATH}" --es-url "${ES_URL}" --kibana-url "${KIBANA_URL}"`, - { failOnNonZeroExit: false } + { env: { NODE_TLS_REJECT_UNAUTHORIZED }, failOnNonZeroExit: false } ); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/inspect.ts b/x-pack/plugins/security_solution/cypress/tasks/inspect.ts index d056e012bb7f8c..112bf9a208b39c 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/inspect.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/inspect.ts @@ -11,6 +11,10 @@ export const closesModal = () => { cy.get('[data-test-subj="modal-inspect-close"]').click(); }; +export const clickInspectButton = (container: string) => { + cy.get(`${container} ${INSPECT_BUTTON_ICON}`).trigger('click', { force: true }); +}; + export const openStatsAndTables = (table: InspectButtonMetadata) => { if (table.tabId) { cy.get(table.tabId).click({ force: true }); @@ -21,6 +25,6 @@ export const openStatsAndTables = (table: InspectButtonMetadata) => { force: true, }); } else { - cy.get(`${table.id} ${INSPECT_BUTTON_ICON}`).trigger('click', { force: true }); + clickInspectButton(table.id); } }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index be447993273fbe..243bfd113bfd23 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -10,6 +10,7 @@ import Url, { UrlObject } from 'url'; import { ROLES } from '../../common/test'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; +import { hostDetailsUrl } from '../urls/navigation'; /** * Credentials in the `kibana.dev.yml` config file will be used to authenticate @@ -312,6 +313,11 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; +export const loginAndWaitForHostDetailsPage = () => { + loginAndWaitForPage(hostDetailsUrl('suricata-iowa')); + cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist'); +}; + export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => { cy.visit(role ? getUrlWithRoute(role, url) : url); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 304db7e93e2cb7..a9fad865f506c3 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -23,6 +23,8 @@ export const SECURITY_DETECTIONS_RULES_CREATION_URL = '/app/security/detections/ export const EXCEPTIONS_URL = 'app/security/exceptions'; export const HOSTS_URL = '/app/security/hosts/allHosts'; +export const hostDetailsUrl = (hostName: string) => + `/app/security/hosts/${hostName}/authentications`; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/security/hosts/allHosts', anomalies: '/app/security/hosts/anomalies', diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index cbaa789d47489b..e1c14f2a863803 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -26,7 +26,7 @@ import { NETWORK, TIMELINES, CASE, - ADMINISTRATION, + MANAGE, } from '../translations'; import { OVERVIEW_PATH, @@ -116,12 +116,12 @@ export const topDeepLinks: AppDeepLink[] = [ }, { id: SecurityPageName.administration, - title: ADMINISTRATION, + title: MANAGE, path: ENDPOINTS_PATH, navLinkStatus: AppNavLinkStatus.hidden, keywords: [ - i18n.translate('xpack.securitySolution.search.administration', { - defaultMessage: 'Administration', + i18n.translate('xpack.securitySolution.search.manage', { + defaultMessage: 'Manage', }), ], }, @@ -165,7 +165,7 @@ const nestedDeepLinks: SecurityDeepLinks = { navLinkStatus: AppNavLinkStatus.hidden, keywords: [ i18n.translate('xpack.securitySolution.search.exceptions', { - defaultMessage: 'Exceptions', + defaultMessage: 'Exception list', }), ], searchable: true, @@ -179,28 +179,28 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.hosts.authentications', { defaultMessage: 'Authentications', }), - path: '/authentications', + path: `${HOSTS_PATH}/authentications`, }, { id: 'uncommonProcesses', title: i18n.translate('xpack.securitySolution.search.hosts.uncommonProcesses', { defaultMessage: 'Uncommon Processes', }), - path: '/uncommonProcesses', + path: `${HOSTS_PATH}/uncommonProcesses`, }, { id: 'events', title: i18n.translate('xpack.securitySolution.search.hosts.events', { defaultMessage: 'Events', }), - path: '/events', + path: `${HOSTS_PATH}/events`, }, { id: 'externalAlerts', title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', { defaultMessage: 'External Alerts', }), - path: '/alerts', + path: `${HOSTS_PATH}/alerts`, }, ], premium: [ @@ -209,7 +209,7 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', { defaultMessage: 'Anomalies', }), - path: '/anomalies', + path: `${HOSTS_PATH}/anomalies`, }, ], }, @@ -220,28 +220,28 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.network.dns', { defaultMessage: 'DNS', }), - path: '/dns', + path: `${NETWORK_PATH}/dns`, }, { id: 'http', title: i18n.translate('xpack.securitySolution.search.network.http', { defaultMessage: 'HTTP', }), - path: '/http', + path: `${NETWORK_PATH}/http`, }, { id: 'tls', title: i18n.translate('xpack.securitySolution.search.network.tls', { defaultMessage: 'TLS', }), - path: '/tls', + path: `${NETWORK_PATH}/tls`, }, { id: 'externalAlertsNetwork', title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', { defaultMessage: 'External Alerts', }), - path: '/external-alerts', + path: `${NETWORK_PATH}/external-alerts`, }, ], premium: [ @@ -250,7 +250,7 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', { defaultMessage: 'Anomalies', }), - path: '/anomalies', + path: `${NETWORK_PATH}/anomalies`, }, ], }, @@ -261,7 +261,7 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.timeline.templates', { defaultMessage: 'Templates', }), - path: '/template', + path: `${TIMELINES_PATH}/template`, }, ], }, @@ -272,7 +272,7 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.cases.create', { defaultMessage: 'Create New Case', }), - path: '/create', + path: `${CASES_PATH}/create`, }, ], premium: [ @@ -281,7 +281,7 @@ const nestedDeepLinks: SecurityDeepLinks = { title: i18n.translate('xpack.securitySolution.search.cases.configure', { defaultMessage: 'Configure Cases', }), - path: '/configure', + path: `${CASES_PATH}/configure`, }, ], }, @@ -299,14 +299,14 @@ const nestedDeepLinks: SecurityDeepLinks = { { id: SecurityPageName.trustedApps, title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', { - defaultMessage: 'Trusted Applications', + defaultMessage: 'Trusted applications', }), path: TRUSTED_APPS_PATH, }, { id: SecurityPageName.eventFilters, title: i18n.translate('xpack.securitySolution.search.administration.eventFilters', { - defaultMessage: 'Event Filters', + defaultMessage: 'Event filters', }), path: EVENT_FILTERS_PATH, }, @@ -377,12 +377,13 @@ export function updateGlobalNavigation({ const deepLinks = getDeepLinks(undefined, capabilities); const updatedDeepLinks = deepLinks.map((link) => { switch (link.id) { - case 'case': + case SecurityPageName.case: return { ...link, navLinkStatus: capabilities.siem.read_cases ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + searchable: capabilities.siem.read_cases === true, }; default: return link; @@ -390,6 +391,7 @@ export function updateGlobalNavigation({ }); updater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent showing main nav link deepLinks: updatedDeepLinks, })); } diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index 271eea47840dc0..d6f8516d43a725 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -6,8 +6,11 @@ */ import * as i18n from '../translations'; -import { SecurityPageName, SecurityPageGroupName } from '../types'; -import { SiemNavTab, NavTabGroups } from '../../common/components/navigation/types'; +import { + SecurityNav, + SecurityNavGroup, + SecurityNavGroupKey, +} from '../../common/components/navigation/types'; import { APP_OVERVIEW_PATH, APP_RULES_PATH, @@ -21,9 +24,10 @@ import { APP_ENDPOINTS_PATH, APP_TRUSTED_APPS_PATH, APP_EVENT_FILTERS_PATH, + SecurityPageName, } from '../../../common/constants'; -export const navTabs: SiemNavTab = { +export const navTabs: SecurityNav = { [SecurityPageName.overview]: { id: SecurityPageName.overview, name: i18n.OVERVIEW, @@ -36,21 +40,21 @@ export const navTabs: SiemNavTab = { name: i18n.ALERTS, href: APP_ALERTS_PATH, disabled: false, - urlKey: SecurityPageName.alerts, + urlKey: 'alerts', }, [SecurityPageName.rules]: { id: SecurityPageName.rules, name: i18n.RULES, href: APP_RULES_PATH, disabled: false, - urlKey: SecurityPageName.rules, + urlKey: 'rules', }, [SecurityPageName.exceptions]: { id: SecurityPageName.exceptions, name: i18n.EXCEPTIONS, href: APP_EXCEPTIONS_PATH, disabled: false, - urlKey: SecurityPageName.exceptions, + urlKey: 'exceptions', }, [SecurityPageName.hosts]: { id: SecurityPageName.hosts, @@ -85,46 +89,46 @@ export const navTabs: SiemNavTab = { name: i18n.ADMINISTRATION, href: APP_MANAGEMENT_PATH, disabled: false, - urlKey: SecurityPageName.administration, + urlKey: 'administration', }, [SecurityPageName.endpoints]: { id: SecurityPageName.endpoints, name: i18n.ENDPOINTS, href: APP_ENDPOINTS_PATH, disabled: false, - urlKey: SecurityPageName.administration, + urlKey: 'administration', }, [SecurityPageName.trustedApps]: { id: SecurityPageName.trustedApps, name: i18n.TRUSTED_APPLICATIONS, href: APP_TRUSTED_APPS_PATH, disabled: false, - urlKey: SecurityPageName.administration, + urlKey: 'administration', }, [SecurityPageName.eventFilters]: { id: SecurityPageName.eventFilters, name: i18n.EVENT_FILTERS, href: APP_EVENT_FILTERS_PATH, disabled: false, - urlKey: SecurityPageName.administration, + urlKey: 'administration', }, }; -export const navTabGroups: NavTabGroups = { - [SecurityPageGroupName.detect]: { - id: SecurityPageGroupName.detect, +export const securityNavGroup: SecurityNavGroup = { + [SecurityNavGroupKey.detect]: { + id: SecurityNavGroupKey.detect, name: i18n.DETECT, }, - [SecurityPageGroupName.explore]: { - id: SecurityPageGroupName.explore, + [SecurityNavGroupKey.explore]: { + id: SecurityNavGroupKey.explore, name: i18n.EXPLORE, }, - [SecurityPageGroupName.investigate]: { - id: SecurityPageGroupName.investigate, + [SecurityNavGroupKey.investigate]: { + id: SecurityNavGroupKey.investigate, name: i18n.INVESTIGATE, }, - [SecurityPageGroupName.manage]: { - id: SecurityPageGroupName.manage, + [SecurityNavGroupKey.manage]: { + id: SecurityNavGroupKey.manage, name: i18n.MANAGE, }, }; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index d16c35a832e6b8..e7f825691c58d7 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -14,12 +14,14 @@ import { SecuritySolutionAppWrapper } from '../../common/components/page'; import { HelpMenu } from '../../common/components/help_menu'; import { UseUrlState } from '../../common/components/url_state'; import { navTabs } from './home_navigations'; -import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { + useInitSourcerer, + useSourcererScope, + getScopeFromPath, +} from '../../common/containers/sourcerer'; import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages'; import { GlobalHeader } from './global_header'; import { SecuritySolutionTemplateWrapper } from './template_wrapper'; -import { isDetectionsPath } from '../../helpers'; interface HomePageProps { children: React.ReactNode; @@ -34,13 +36,9 @@ const HomePageComponent: React.FC = ({ }) => { const { pathname } = useLocation(); - useInitSourcerer( - isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default - ); + useInitSourcerer(getScopeFromPath(pathname)); - const { browserFields, indexPattern } = useSourcererScope( - isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default - ); + const { browserFields, indexPattern } = useSourcererScope(getScopeFromPath(pathname)); // side effect: this will attempt to upgrade the endpoint package if it is not up to date // this will run when a user navigates to the Security Solution app and when they navigate between diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index a2f1ed8c115d6b..eb606cd8ff583b 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -12,12 +12,10 @@ import { useLocation } from 'react-router-dom'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppLeaveHandler } from '../../../../../../../../src/core/public'; import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline'; -import { useSourcererScope } from '../../../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { useSourcererScope, getScopeFromPath } from '../../../../common/containers/sourcerer'; import { TimelineId } from '../../../../../common/types/timeline'; import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning'; import { Flyout } from '../../../../timelines/components/flyout'; -import { isDetectionsPath } from '../../../../../public/helpers'; export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar'; @@ -27,9 +25,7 @@ export const SecuritySolutionBottomBar = React.memo( const [showTimeline] = useShowTimeline(); - const { indicesExist } = useSourcererScope( - isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default - ); + const { indicesExist } = useSourcererScope(getScopeFromPath(pathname)); return indicesExist && showTimeline ? ( <> diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index edbab928a5c694..847a9114d94bd4 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -24,7 +24,7 @@ export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { }); export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { - defaultMessage: 'Exceptions', + defaultMessage: 'Exception list', }); export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { @@ -48,13 +48,13 @@ export const ENDPOINTS = i18n.translate('xpack.securitySolution.search.administr export const TRUSTED_APPLICATIONS = i18n.translate( 'xpack.securitySolution.search.administration.trustedApps', { - defaultMessage: 'Trusted Applications', + defaultMessage: 'Trusted applications', } ); export const EVENT_FILTERS = i18n.translate( 'xpack.securitySolution.search.administration.eventFilters', { - defaultMessage: 'Event Filters', + defaultMessage: 'Event filters', } ); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 62a61828830bed..8056c4092091ce 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -36,7 +36,7 @@ import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; import { TimelineState } from '../timelines/store/timeline/types'; import { SecurityPageName } from '../../common/constants'; -export { SecurityPageName, SecurityPageGroupName } from '../../common/constants'; +export { SecurityPageName } from '../../common/constants'; export interface SecuritySubPluginStore { initialState: Record; diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index 13a549b6babc9d..c5ed3454f1ca57 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -19,7 +19,6 @@ import { CaseHeaderPage } from '../components/case_header_page'; import { WhitePageWrapper, SectionWrapper } from '../components/wrappers'; import * as i18n from './translations'; import { APP_ID } from '../../../common/constants'; -import { SiemNavTabKey } from '../../common/components/navigation/types'; const ConfigureCasesPageComponent: React.FC = () => { const { @@ -31,9 +30,9 @@ const ConfigureCasesPageComponent: React.FC = () => { const backOptions = useMemo( () => ({ - href: getCaseUrl(search), + path: getCaseUrl(search), text: i18n.BACK_TO_ALL, - pageId: SecurityPageName.case as SiemNavTabKey, + pageId: SecurityPageName.case, }), [search] ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index e46e5c2074f05f..4a59fe3fdcabdf 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -18,7 +18,6 @@ import { CaseHeaderPage } from '../components/case_header_page'; import { Create } from '../components/create'; import * as i18n from './translations'; import { APP_ID } from '../../../common/constants'; -import { SiemNavTabKey } from '../../common/components/navigation/types'; export const CreateCasePage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); @@ -29,9 +28,9 @@ export const CreateCasePage = React.memo(() => { const backOptions = useMemo( () => ({ - href: getCaseUrl(search), + path: getCaseUrl(search), text: i18n.BACK_TO_ALL, - pageId: SecurityPageName.case as SiemNavTabKey, + pageId: SecurityPageName.case, }), [search] ); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx index a5e0c90402df42..ebd25eef87cb7f 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx @@ -9,8 +9,6 @@ import React, { memo, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { AppLocation } from '../../../../common/endpoint/types'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { TimelineId } from '../../../../../timelines/common'; /** * This component should be used above all routes, but below the Provider. @@ -20,10 +18,6 @@ export const RouteCapture = memo(({ children }) => { const location: AppLocation = useLocation(); const dispatch = useDispatch(); - useEffect(() => { - dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); - }, [dispatch, location.pathname]); - useEffect(() => { dispatch({ type: 'userChangedUrl', payload: location }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index d07cdd81aa5f45..9afaaef61b17a4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -104,7 +104,7 @@ const EventDetailsComponent: React.FC = ({ setSelectedTabId, ]); - const eventFields = useMemo(() => getEnrichmentFields(data ?? []), [data]); + const eventFields = useMemo(() => getEnrichmentFields(data), [data]); const existingEnrichments = useMemo( () => isAlert @@ -242,7 +242,7 @@ const EventDetailsComponent: React.FC = ({ ); }, [summaryTab, threatIntelTab, tableTab, jsonTab]); - const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [ + const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0], [ tabs, selectedTabId, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 2c42353daee75f..47b08712298642 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -41,7 +41,7 @@ describe('HeaderPage', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index dc8e19249b6be0..dea19e1366875c 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -12,7 +12,7 @@ import { EuiPageHeaderSection, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React from 'react'; import styled, { css } from 'styled-components'; import { LinkIcon, LinkIconProps } from '../link_icon'; @@ -24,8 +24,6 @@ import { SecurityPageName } from '../../../app/types'; import { Sourcerer } from '../sourcerer'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useKibana } from '../../lib/kibana'; -import { SiemNavTabKey } from '../navigation/types'; - interface HeaderProps { border?: boolean; isLoading?: boolean; @@ -65,10 +63,10 @@ const HeaderSection = styled(EuiPageHeaderSection)` HeaderSection.displayName = 'HeaderSection'; interface BackOptions { + pageId: SecurityPageName; text: LinkIconProps['children']; - href?: LinkIconProps['href']; + path?: string; dataTestSubj?: string; - pageId: SiemNavTabKey; } export interface HeaderPageProps extends HeaderProps { @@ -85,6 +83,29 @@ export interface HeaderPageProps extends HeaderProps { titleNode?: React.ReactElement; } +const HeaderLinkBack: React.FC<{ backOptions: BackOptions }> = React.memo(({ backOptions }) => { + const { navigateToUrl } = useKibana().services.application; + const { formatUrl } = useFormatUrl(backOptions.pageId); + + const backUrl = formatUrl(backOptions.path ?? ''); + return ( + + { + ev.preventDefault(); + navigateToUrl(backUrl); + }} + href={backUrl} + iconType="arrowLeft" + > + {backOptions.text} + + + ); +}); +HeaderLinkBack.displayName = 'HeaderLinkBack'; + const HeaderPageComponent: React.FC = ({ backOptions, backComponent, @@ -99,62 +120,36 @@ const HeaderPageComponent: React.FC = ({ title, titleNode, ...rest -}) => { - const { navigateToUrl } = useKibana().services.application; +}) => ( + <> + + + {backOptions && } + {!backOptions && backComponent && <>{backComponent}} - const { formatUrl } = useFormatUrl(backOptions?.pageId ?? SecurityPageName.overview); - const backUrl = formatUrl(backOptions?.href ?? ''); - const goTo = useCallback( - (ev) => { - ev.preventDefault(); - if (backOptions) { - navigateToUrl(backUrl); - } - }, - [backOptions, navigateToUrl, backUrl] - ); - return ( - <> - - - {backOptions && ( - - - {backOptions.text} - - - )} - - {!backOptions && backComponent && <>{backComponent}} - {titleNode || ( - - )} + {titleNode || ( + <Title + draggableArguments={draggableArguments} + title={title} + badgeOptions={badgeOptions} + /> + )} - {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} - {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} - {border && isLoading && <EuiProgress size="xs" color="accent" />} - </HeaderSection> + {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} + {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} + {border && isLoading && <EuiProgress size="xs" color="accent" />} + </HeaderSection> - {children && ( - <EuiPageHeaderSection data-test-subj="header-page-supplements"> - {children} - </EuiPageHeaderSection> - )} - {!hideSourcerer && <Sourcerer scope={SourcererScopeName.default} />} - </EuiPageHeader> - {/* Manually add a 'padding-bottom' to header */} - <EuiSpacer size="l" /> - </> - ); -}; + {children && ( + <EuiPageHeaderSection data-test-subj="header-page-supplements"> + {children} + </EuiPageHeaderSection> + )} + {!hideSourcerer && <Sourcerer scope={SourcererScopeName.default} />} + </EuiPageHeader> + {/* Manually add a 'padding-bottom' to header */} + <EuiSpacer size="l" /> + </> +); export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index b7defcc8c2af9f..6681ee2cb7e8f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -9,9 +9,9 @@ import { isEmpty } from 'lodash/fp'; import { useCallback } from 'react'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { navTabs } from '../../../app/home/home_navigations'; -import { APP_ID } from '../../../../common/constants'; -import { useKibana } from '../../lib/kibana'; -import { SiemNavTabKey } from '../navigation/types'; +import { useAppUrl } from '../../lib/kibana/hooks'; +import { SecurityNavKey } from '../navigation/types'; +import { SecurityPageName } from '../../../app/types'; export { getDetectionEngineUrl, getRuleDetailsUrl } from './redirect_to_detection_engine'; export { getAppOverviewUrl } from './redirect_to_overview'; @@ -33,9 +33,11 @@ interface FormatUrlOptions { export type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string; -export const useFormatUrl = (page: SiemNavTabKey) => { - const { getUrlForApp } = useKibana().services.application; - const search = useGetUrlSearch(navTabs[page]); +export const useFormatUrl = (page: SecurityPageName) => { + const { getAppUrl } = useAppUrl(); + const tab = page in navTabs ? navTabs[page as SecurityNavKey] : undefined; + const search = useGetUrlSearch(tab); + const formatUrl = useCallback<FormatUrl>( (path: string, { absolute = false, skipSearch = false } = {}) => { const pathArr = path.split('?'); @@ -48,9 +50,9 @@ export const useFormatUrl = (page: SiemNavTabKey) => { ? '' : `?${pathArr[1]}` }`; - return getUrlForApp(APP_ID, { deepLinkId: page, path: formattedPath, absolute }); + return getAppUrl({ deepLinkId: page, path: formattedPath, absolute }); }, - [getUrlForApp, page, search] + [getAppUrl, page, search] ); return { formatUrl, search }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 1b1b3c9af4bfca..e147e8b7fc9586 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -6,7 +6,7 @@ */ import { UrlStateType } from '../url_state/constants'; -import { SecurityPageName, SecurityPageGroupName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import { UrlState } from '../url_state/types'; import { SiemRouteType } from '../../utils/route/types'; @@ -27,15 +27,14 @@ export interface NavGroupTab { id: string; name: string; } +export enum SecurityNavGroupKey { + detect = 'detect', + explore = 'explore', + investigate = 'investigate', + manage = 'manage', +} -export type SecurityNavTabGroupKey = - | SecurityPageGroupName.detect - | SecurityPageGroupName.explore - | SecurityPageGroupName.investigate - | SecurityPageGroupName.manage; - -export type NavTabGroups = Record<SecurityNavTabGroupKey, NavGroupTab>; - +export type SecurityNavGroup = Record<SecurityNavGroupKey, NavGroupTab>; export interface NavTab { id: string; name: string; @@ -44,8 +43,7 @@ export interface NavTab { urlKey?: UrlStateType; pageId?: SecurityPageName; } - -export type SiemNavTabKey = +export type SecurityNavKey = | SecurityPageName.overview | SecurityPageName.hosts | SecurityPageName.network @@ -59,7 +57,7 @@ export type SiemNavTabKey = | SecurityPageName.trustedApps | SecurityPageName.eventFilters; -export type SiemNavTab = Record<SiemNavTabKey, NavTab>; +export type SecurityNav = Record<SecurityNavKey, NavTab>; export type GetUrlForApp = ( appId: string, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx index 011378db1b31cd..258dad531837a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx @@ -12,9 +12,9 @@ import { makeMapStateToProps } from '../url_state/helpers'; import { getSearch } from './helpers'; import { SearchNavTab } from './types'; -export const useGetUrlSearch = (tab: SearchNavTab) => { +export const useGetUrlSearch = (tab?: SearchNavTab) => { const mapState = makeMapStateToProps(); const { urlState } = useDeepEqualSelector(mapState); - const urlSearch = useMemo(() => getSearch(tab, urlState), [tab, urlState]); + const urlSearch = useMemo(() => (tab ? getSearch(tab, urlState) : ''), [tab, urlState]); return urlSearch; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 7e211a2e95152f..e3549aa6ec0478 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -7,7 +7,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; -import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { useKibana } from '../../../lib/kibana/kibana_react'; +import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { SecurityPageName } from '../../../../app/types'; import { useSecuritySolutionNavigation } from '.'; import { CONSTANTS } from '../../url_state/constants'; @@ -16,6 +17,7 @@ import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { UrlInputsModel } from '../../../store/inputs/model'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; +jest.mock('../../../lib/kibana/kibana_react'); jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../utils/route/use_route_spy'); @@ -94,7 +96,7 @@ describe('useSecuritySolutionNavigation', () => { "icon": "logoSecurity", "items": Array [ Object { - "id": "securitySolution", + "id": "main", "items": Array [ Object { "data-href": "securitySolution/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", @@ -139,7 +141,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "exceptions", "isSelected": false, - "name": "Exceptions", + "name": "Exception list", "onClick": [Function], }, ], @@ -207,7 +209,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/trusted_apps", "id": "trusted_apps", "isSelected": false, - "name": "Trusted Applications", + "name": "Trusted applications", "onClick": [Function], }, Object { @@ -217,7 +219,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/event_filters", "id": "event_filters", "isSelected": false, - "name": "Event Filters", + "name": "Event filters", "onClick": [Function], }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index e04ec7727a08f7..fffe59fceff41c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -7,11 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; -import { navTabGroups } from '../../../../app/home/home_navigations'; -import { APP_ID } from '../../../../../common/constants'; +import { securityNavGroup } from '../../../../app/home/home_navigations'; import { getSearch } from '../helpers'; import { PrimaryNavigationItemsProps } from './types'; -import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { useGetUserCasesPermissions } from '../../../lib/kibana'; +import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; export const usePrimaryNavigationItems = ({ @@ -19,7 +19,7 @@ export const usePrimaryNavigationItems = ({ selectedTabId, ...urlStateProps }: PrimaryNavigationItemsProps): Array<EuiSideNavItemType<{}>> => { - const { navigateToApp, getUrlForApp } = useKibana().services.application; + const { navigateTo, getAppUrl } = useNavigation(); const getSideNav = useCallback( (tab: NavTab) => { @@ -29,10 +29,10 @@ export const usePrimaryNavigationItems = ({ const handleClick = (ev: React.MouseEvent) => { ev.preventDefault(); - navigateToApp(APP_ID, { deepLinkId: id, path: urlSearch }); + navigateTo({ deepLinkId: id, path: urlSearch }); }; - const appHref = getUrlForApp(APP_ID, { deepLinkId: id, path: urlSearch }); + const appHref = getAppUrl({ deepLinkId: id, path: urlSearch }); return { 'data-href': appHref, @@ -45,7 +45,7 @@ export const usePrimaryNavigationItems = ({ onClick: handleClick, }; }, - [getUrlForApp, navigateToApp, selectedTabId, urlStateProps] + [getAppUrl, navigateTo, selectedTabId, urlStateProps] ); const navItemsToDisplay = usePrimaryNavigationItemsToDisplay(navTabs); @@ -63,27 +63,30 @@ export const usePrimaryNavigationItems = ({ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) { const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - return [ - { - id: APP_ID, - name: '', - items: [navTabs.overview], - }, - { - ...navTabGroups.detect, - items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], - }, - { - ...navTabGroups.explore, - items: [navTabs.hosts, navTabs.network], - }, - { - ...navTabGroups.investigate, - items: hasCasesReadPermissions ? [navTabs.timelines, navTabs.case] : [navTabs.timelines], - }, - { - ...navTabGroups.manage, - items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], - }, - ]; + return useMemo( + () => [ + { + id: 'main', + name: '', + items: [navTabs.overview], + }, + { + ...securityNavGroup.detect, + items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], + }, + { + ...securityNavGroup.explore, + items: [navTabs.hosts, navTabs.network], + }, + { + ...securityNavGroup.investigate, + items: hasCasesReadPermissions ? [navTabs.timelines, navTabs.case] : [navTabs.timelines], + }, + { + ...securityNavGroup.manage, + items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], + }, + ], + [navTabs, hasCasesReadPermissions] + ); } diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index b40799895e8a2c..18b99adca3a552 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -59,6 +59,14 @@ jest.mock('../../lib/kibana', () => ({ }, })); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('UrlStateContainer', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index e178aba188d11a..3175656f120710 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -31,6 +31,14 @@ jest.mock('../../lib/kibana', () => ({ }), })); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('UrlStateContainer - lodash.throttle mocked to test update url', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 487463dfd9d7dc..87e17ba7691cc5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -9,6 +9,7 @@ import { difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; import { useKibana } from '../../lib/kibana'; import { CONSTANTS, UrlStateType } from './constants'; import { @@ -31,6 +32,8 @@ import { UrlState, } from './types'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../../timelines/common'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef<PreviousLocationUrlState>(value); @@ -71,6 +74,7 @@ export const useUrlStateHooks = ({ const [isInitializing, setIsInitializing] = useState(true); const { filterManager, savedQueries } = useKibana().services.data.query; const prevProps = usePrevious({ pathName, pageName, urlState }); + const dispatch = useDispatch(); const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { let mySearch = search; @@ -222,9 +226,10 @@ export const useUrlStateHooks = ({ }); } else if (pathName !== prevProps.pathName) { handleInitialize(type, isDetectionsPages(pageName)); + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInitializing, history, pathName, pageName, prevProps, urlState]); + }, [isInitializing, history, pathName, pageName, prevProps, urlState, dispatch]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 702a532949428d..ae2e509a7d94ee 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Provider } from 'react-redux'; -import { useInitSourcerer } from '.'; +import { getScopeFromPath, useInitSourcerer } from '.'; import { mockPatterns } from './mocks'; // import { SourcererScopeName } from '../../store/sourcerer/model'; import { RouteSpyState } from '../../utils/route/types'; @@ -180,3 +180,18 @@ describe('Sourcerer Hooks', () => { }); }); }); + +describe('getScopeFromPath', () => { + it('should return default scope', async () => { + expect(getScopeFromPath('/')).toBe(SourcererScopeName.default); + expect(getScopeFromPath('/exceptions')).toBe(SourcererScopeName.default); + expect(getScopeFromPath('/rules')).toBe(SourcererScopeName.default); + expect(getScopeFromPath('/rules/create')).toBe(SourcererScopeName.default); + }); + + it('should return detections scope', async () => { + expect(getScopeFromPath('/alerts')).toBe(SourcererScopeName.detections); + expect(getScopeFromPath('/rules/id/foo')).toBe(SourcererScopeName.detections); + expect(getScopeFromPath('/rules/id/foo/edit')).toBe(SourcererScopeName.detections); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 8cc075de324a24..002c40fc9d428c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -8,11 +8,13 @@ import { useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; +import { matchPath } from 'react-router-dom'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIndexFields } from '../source'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; +import { ALERTS_PATH, RULES_PATH } from '../../../../common/constants'; import { TimelineId } from '../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../hooks/use_selector'; @@ -125,3 +127,14 @@ export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName const SourcererScope = useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); return SourcererScope; }; + +export const getScopeFromPath = ( + pathname: string +): SourcererScopeName.default | SourcererScopeName.detections => { + return matchPath(pathname, { + path: [ALERTS_PATH, `${RULES_PATH}/id/:id`], + strict: false, + }) == null + ? SourcererScopeName.default + : SourcererScopeName.detections; +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index 1b05c6a857263c..b9bbf7afd36265 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -15,6 +15,7 @@ import { set } from '@elastic/safer-lodash-set'; import { APP_ID, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { AuthenticatedUser } from '../../../../../security/common/model'; +import { NavigateToAppOptions } from '../../../../../../../src/core/public'; import { StartServices } from '../../../types'; import { useUiSetting, useKibana } from './kibana_react'; @@ -169,8 +170,15 @@ export const useAppUrl = () => { const { getUrlForApp } = useKibana().services.application; const getAppUrl = useCallback( - ({ appId = APP_ID, ...options }: { appId?: string; deepLinkId?: string; path?: string }) => - getUrlForApp(appId, options), + ({ + appId = APP_ID, + ...options + }: { + appId?: string; + deepLinkId?: string; + path?: string; + absolute?: boolean; + }) => getUrlForApp(appId, options), [getUrlForApp] ); return { getAppUrl }; @@ -191,9 +199,7 @@ export const useNavigateTo = () => { }: { url?: string; appId?: string; - deepLinkId?: string; - path?: string; - }) => { + } & NavigateToAppOptions) => { if (url) { navigateToUrl(url); } else { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 31d1ce6d411536..70101021bc4f0a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -27,8 +27,8 @@ jest.mock('react-router-dom', () => { }); const mockNavigateToApp = jest.fn(); -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); +jest.mock('../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../common/lib/kibana/kibana_react'); return { ...original, @@ -43,6 +43,13 @@ jest.mock('../../../common/lib/kibana', () => { }, }, }), + }; +}); + +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + return { + ...original, useUiSetting$: jest.fn().mockReturnValue([]), useGetUserSavedObjectPermissions: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index 880817af856f8f..0595fd96d1377f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -27,7 +27,20 @@ jest.mock('react-router-dom', () => { }); jest.mock('../../../../common/components/link_to'); -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); + return { + ...original, + useKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + }, + }, + }), + }; +}); jest.mock('../../../containers/detection_engine/rules/api', () => ({ getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 5688b4065ab764..90568e28793a37 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -16,8 +16,7 @@ import { SecurityPageName } from '../../../../app/types'; import { useFormatUrl } from '../../../../common/components/link_to'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { useUserData } from '../../user_info'; -import { APP_ID } from '../../../../../common/constants'; -import { useKibana } from '../../../../common/lib/kibana'; +import { useNavigateTo } from '../../../../common/lib/kibana/hooks'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -40,14 +39,14 @@ const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = ( createPrePackagedRules(); }, [createPrePackagedRules]); const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const { navigateToApp } = useKibana().services.application; + const { navigateTo } = useNavigateTo(); const goToCreateRule = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(APP_ID, { deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() }); + navigateTo({ deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() }); }, - [navigateToApp] + [navigateTo] ); const [ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 3d488f1f08c983..1f2bda768d19c5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -305,7 +305,7 @@ const CreateRulePageComponent: React.FC = () => { <MaxWidthEuiFlexItem> <DetectionEngineHeaderPage backOptions={{ - href: getRulesUrl(), + path: getRulesUrl(), text: i18n.BACK_TO_RULES, pageId: SecurityPageName.rules, }} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index a7a9b31d1f408a..66f62ad3ebeab8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -578,7 +578,7 @@ const RuleDetailsPageComponent = () => { <Display show={!globalFullScreen}> <DetectionEngineHeaderPage backOptions={{ - href: getRulesUrl(), + path: getRulesUrl(), text: i18n.BACK_TO_RULES, pageId: SecurityPageName.rules, dataTestSubj: 'ruleDetailsBackToAllRules', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 4786d7f2eae788..caec85f537d2ba 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -355,7 +355,7 @@ const EditRulePageComponent: FC = () => { <MaxWidthEuiFlexItem> <DetectionEngineHeaderPage backOptions={{ - href: getRuleDetailsUrl(ruleId ?? ''), + path: getRuleDetailsUrl(ruleId ?? ''), text: `${i18n.BACK_TO} ${rule?.name ?? ''}`, pageId: SecurityPageName.rules, dataTestSubj: 'ruleEditBackToRuleDetails', diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 6e371bbf610e1e..7c34e6f30b9102 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -97,7 +97,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta ); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); - const [loading, { hostDetails: hostOverview, id, refetch }] = useHostDetails({ + const [loading, { inspect, hostDetails: hostOverview, id, refetch }] = useHostDetails({ endDate: to, startDate: from, hostName: detailName, @@ -169,6 +169,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta }} setQuery={setQuery} refetch={refetch} + inspect={inspect} /> )} </AnomalyTableProvider> diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 6cf5e989fb645d..a123f06f62f96c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -304,7 +304,7 @@ describe('endpoint list middleware', () => { }); }); - describe('handle Endpoint Pending Actions state actions', () => { + describe.skip('handle Endpoint Pending Actions state actions', () => { let mockedApis: ReturnType<typeof endpointPageHttpMock>; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 2f8ced9d2a771f..e34e9cf5a83f38 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -166,6 +166,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState }); } } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use // Ignore Errors, since this should not hinder the user's ability to use the UI logError(error); } @@ -286,6 +288,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState }); } } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use // Ignore Errors, since this should not hinder the user's ability to use the UI logError(error); } @@ -331,6 +335,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState }); } } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use // Ignore Errors, since this should not hinder the user's ability to use the UI logError(error); } @@ -537,6 +543,8 @@ const endpointsTotal = async (http: HttpStart): Promise<number> => { }) ).total; } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use logError(`error while trying to check for total endpoints`); logError(error); } @@ -547,6 +555,8 @@ const doEndpointsExist = async (http: HttpStart): Promise<boolean> => { try { return (await endpointsTotal(http)) > 0; } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use logError(`error while trying to check if endpoints exist`); logError(error); } @@ -613,6 +623,8 @@ async function getEndpointPackageInfo( payload: createLoadedResourceState(packageInfo), }); } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use // Ignore Errors, since this should not hinder the user's ability to use the UI logError(error); dispatch({ @@ -663,6 +675,8 @@ const loadEndpointsPendingActions = async ({ payload: createLoadedResourceState(agentIdToPendingActions), }); } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use logError(error); } }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx index 6a106b14886772..d5a1c6624923b2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.test.tsx @@ -69,9 +69,8 @@ describe('Event filter flyout', () => { it('should renders correctly', () => { const component = render(); - expect(component.getAllByText('Add Endpoint Event Filter')).not.toBeNull(); + expect(component.getAllByText('Add event filter')).not.toBeNull(); expect(component.getByText('Cancel')).not.toBeNull(); - expect(component.getByText('Endpoint Security')).not.toBeNull(); }); it('should dispatch action to init form store on mount', async () => { @@ -183,9 +182,8 @@ describe('Event filter flyout', () => { it('should renders correctly when id and edit type', () => { const component = render({ id: 'fakeId', type: 'edit' }); - expect(component.getAllByText('Update Endpoint Event Filter')).not.toBeNull(); + expect(component.getAllByText('Update event filter')).not.toBeNull(); expect(component.getByText('Cancel')).not.toBeNull(); - expect(component.getByText('Endpoint Security')).not.toBeNull(); }); it('should dispatch action to init form store on mount with id', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx index 1217488a75ea61..c45741c1520b14 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -112,23 +112,19 @@ export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo( <EuiFlyoutHeader hasBorder> <EuiTitle size="m"> <h2> - <FormattedMessage - id="xpack.securitySolution.eventFilters.eventFiltersFlyout.title" - defaultMessage="Endpoint Security" - /> + {id ? ( + <FormattedMessage + id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update" + defaultMessage="Update event filter" + /> + ) : ( + <FormattedMessage + id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create" + defaultMessage="Add event filter" + /> + )} </h2> </EuiTitle> - {id ? ( - <FormattedMessage - id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.update" - defaultMessage="Update Endpoint Event Filter" - /> - ) : ( - <FormattedMessage - id="xpack.securitySolution.eventFilters.eventFiltersFlyout.subtitle.create" - defaultMessage="Add Endpoint Event Filter" - /> - )} </EuiFlyoutHeader> <EuiFlyoutBody> diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 121808b62f570d..db5c42241a0cc4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -206,7 +206,7 @@ export const EventFiltersForm: React.FC<EventFiltersFormProps> = memo( return !isIndexPatternLoading && exception ? ( <EuiForm component="div"> <EuiText size="s">{FORM_DESCRIPTION}</EuiText> - <EuiSpacer size="s" /> + <EuiSpacer size="m" /> {nameInputMemo} <EuiSpacer size="m" /> {allowSelectOs ? ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_custom_assets_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_custom_assets_extension.tsx new file mode 100644 index 00000000000000..781aa880d1483e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_custom_assets_extension.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { useKibana } from '../../../../../common/lib/kibana'; +import { APP_PATH } from '../../../../../../common/constants'; +import { + CustomAssetsAccordionProps, + CustomAssetsAccordion, + PackageAssetsComponent, +} from '../../../../../../../fleet/public'; + +export const EndpointCustomAssetsExtension: PackageAssetsComponent = () => { + const { http } = useKibana().services; + const views: CustomAssetsAccordionProps['views'] = [ + { + name: i18n.translate('xpack.securitySolution.fleetIntegration.assets.name', { + defaultMessage: 'Hosts', + }), + url: http.basePath.prepend(`${APP_PATH}/administration/endpoints`), + description: i18n.translate('xpack.securitySolution.fleetIntegration.assets.description', { + defaultMessage: 'View endpoints in Security app', + }), + }, + ]; + + return <CustomAssetsAccordion views={views} initialIsOpen />; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension.tsx new file mode 100644 index 00000000000000..05187d64a12e7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +export const LazyEndpointCustomAssetsExtension = lazy(async () => { + const { EndpointCustomAssetsExtension } = await import('./endpoint_custom_assets_extension'); + + return { + default: EndpointCustomAssetsExtension, + }; +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index fa644d1cbcdac2..75723e0d3af84c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -203,7 +203,10 @@ export const HostOverview = React.memo<HostSummaryProps>( return ( <> <InspectButtonContainer> - <OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}> + <OverviewWrapper + direction={isInDetailsSidePanel ? 'column' : 'row'} + data-test-subj="host-overview" + > {!isInDetailsSidePanel && ( <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> )} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 1bf3edf1605d83..137fef16415010 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -60,6 +60,7 @@ import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/vie import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import type { TimelineState } from '../../timelines/public'; +import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { private kibanaVersion: string; @@ -199,19 +200,25 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S registerExtension({ package: 'endpoint', view: 'package-policy-edit', - component: getLazyEndpointPolicyEditExtension(core, plugins), + Component: getLazyEndpointPolicyEditExtension(core, plugins), }); registerExtension({ package: 'endpoint', view: 'package-policy-create', - component: LazyEndpointPolicyCreateExtension, + Component: LazyEndpointPolicyCreateExtension, }); registerExtension({ package: 'endpoint', view: 'package-detail-custom', - component: getLazyEndpointPackageCustomExtension(core, plugins), + Component: getLazyEndpointPackageCustomExtension(core, plugins), + }); + + registerExtension({ + package: 'endpoint', + view: 'package-detail-assets', + Component: LazyEndpointCustomAssetsExtension, }); } licenseService.start(plugins.licensing.license$); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index fff2d4559c8dfc..47f6fe6606d3db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -122,7 +122,7 @@ export const ExpandableEvent = React.memo<Props>( <StyledEuiFlexItem grow={true}> <EventDetails browserFields={browserFields} - data={detailsData!} + data={detailsData ?? []} id={event.eventId!} isAlert={isAlert} timelineId={timelineId} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap index 84611e0b7f02c4..2cd3d333798d4c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap @@ -121,14 +121,17 @@ exports[`Expandable Host Component ExpandableHostDetails: rendering it should re className="c0" > <OverviewWrapper + data-test-subj="host-overview" direction="column" > <EuiFlexGroup className="c1" + data-test-subj="host-overview" direction="column" > <div className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive c1" + data-test-subj="host-overview" > <OverviewDescriptionList descriptionList={ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a32ea7b53f6eec..df454b21ee7255 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8485,7 +8485,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "タイムスタンプフォーマット {timestampFormat} は、疑問符 ({fieldPlaceholder}) が含まれているためサポートされていません", "xpack.dataVisualizer.file.editFlyout.overrides.trimFieldsLabel": "フィールドを切り抜く", "xpack.dataVisualizer.file.editFlyout.overrideSettingsTitle": "上書き設定", - "xpack.dataVisualizer.experimentalBadge.experimentalLabel": "実験的", "xpack.dataVisualizer.file.explanationFlyout.closeButton": "閉じる", "xpack.dataVisualizer.file.explanationFlyout.content": "分析結果を生成した論理ステップ。", "xpack.dataVisualizer.file.explanationFlyout.title": "分析説明", @@ -8589,7 +8588,6 @@ "xpack.dataVisualizer.file.importSummary.indexPatternTitle": "インデックスパターン", "xpack.dataVisualizer.file.importSummary.indexTitle": "インデックス", "xpack.dataVisualizer.file.importSummary.ingestPipelineTitle": "パイプラインを投入", - "xpack.dataVisualizer.file.importView.experimentalFeatureTooltip": "実験的機能。フィードバックをお待ちしています。", "xpack.dataVisualizer.file.importView.importButtonLabel": "インポート", "xpack.dataVisualizer.file.importView.importDataTitle": "データのインポート", "xpack.dataVisualizer.file.importView.importPermissionError": "インデックス {index} にデータを作成またはインポートするパーミッションがありません。", @@ -8624,14 +8622,11 @@ "xpack.dataVisualizer.file.simpleImportSettings.indexNameFormRowLabel": "インデックス名", "xpack.dataVisualizer.file.simpleImportSettings.indexNamePlaceholder": "インデックス名", "xpack.dataVisualizer.file.welcomeContent.delimitedTextFilesDescription": "CSV や TSV などの区切られたテキストファイル", - "xpack.dataVisualizer.file.welcomeContent.experimentalFeatureDescription": "これは実験的な機能です。フィードバックがありますか?{githubLink}で問題を報告してください。", - "xpack.dataVisualizer.file.welcomeContent.experimentalFeatureTooltip": "実験的機能。フィードバックをお待ちしています。", "xpack.dataVisualizer.file.welcomeContent.logFilesWithCommonFormatDescription": "タイムスタンプの一般的フォーマットのログファイル", "xpack.dataVisualizer.file.welcomeContent.newlineDelimitedJsonDescription": "改行区切りの JSON", "xpack.dataVisualizer.file.welcomeContent.supportedFileFormatDescription": "ファイルデータビジュアライザーはこれらのファイル形式をサポートしています:", "xpack.dataVisualizer.file.welcomeContent.uploadedFilesAllowedSizeDescription": "最大{maxFileSize}のファイルをアップロードできます。", "xpack.dataVisualizer.file.welcomeContent.visualizeDataFromLogFileDescription": "ファイルデータビジュアライザーは、ログファイルのフィールドとメトリックの理解に役立ちます。ファイルをアップロードして、データを分析し、 Elasticsearch インデックスにインポートするか選択できます。", - "xpack.dataVisualizer.file.welcomeContent.visualizeDataFromLogFileTitle": "ログファイルのデータを可視化 {experimentalBadge}", "xpack.dataVisualizer.index.actionsPanel.discoverAppTitle": "Discover", "xpack.dataVisualizer.index.actionsPanel.exploreTitle": "データの調査", "xpack.dataVisualizer.index.actionsPanel.viewIndexInDiscoverDescription": "インデックスのドキュメントを調査します。", @@ -14346,8 +14341,6 @@ "xpack.ml.dataVisualizer.fileBasedLabel": "ファイル", "xpack.ml.datavisualizer.selector.dataVisualizerDescription": "機械学習データビジュアライザーツールは、ログファイルのメトリックとフィールド、または既存の Elasticsearch インデックスを分析し、データの理解を助けます。", "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "データビジュアライザー", - "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "実験的", - "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "実験的機能。フィードバックをお待ちしています。", "xpack.ml.datavisualizer.selector.importDataDescription": "ログファイルからデータをインポートします。最大{maxFileSize}のファイルをアップロードできます。", "xpack.ml.datavisualizer.selector.importDataTitle": "データのインポート", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "インデックスパターンを選択", @@ -20818,7 +20811,6 @@ "xpack.securitySolution.rowRenderer.wasPreventedFromExecutingAMaliciousProcessDescription": "悪意のあるファイルの実行が防止されました", "xpack.securitySolution.rowRenderer.wasPreventedFromModifyingAMaliciousFileDescription": "悪意のあるファイルの修正が防止されました", "xpack.securitySolution.rowRenderer.wasPreventedFromRenamingAMaliciousFileDescription": "悪意のあるファイルの名前変更が防止されました", - "xpack.securitySolution.search.administration": "エンドポイント管理", "xpack.securitySolution.search.administration.trustedApps": "信頼できるアプリケーション", "xpack.securitySolution.search.cases": "ケース", "xpack.securitySolution.search.cases.configure": "ケースを構成", @@ -23599,7 +23591,6 @@ "xpack.uptime.monitorDetails.title.pingType.icmp": "ICMP ping", "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "レスポンス異常スコア", - "xpack.uptime.monitorList.defineConnector.description": "アラートを有効にするには、デフォルトのアラートアクションコネクターを定義してください。", "xpack.uptime.monitorList.downLineSeries.downLabel": "ダウン", "xpack.uptime.monitorList.drawer.missingLocation": "一部の Heartbeat インスタンスには位置情報が定義されていません。Heartbeat 構成への{link}。", "xpack.uptime.monitorList.drawer.mostRecentRun": "直近のテスト実行", @@ -24223,4 +24214,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eb96616c53053d..447ba99945edcc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8557,7 +8557,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "时间戳格式 {timestampFormat} 不受支持,因为其包含问号字符 ({fieldPlaceholder})", "xpack.dataVisualizer.file.editFlyout.overrides.trimFieldsLabel": "应剪裁字段", "xpack.dataVisualizer.file.editFlyout.overrideSettingsTitle": "替代设置", - "xpack.dataVisualizer.experimentalBadge.experimentalLabel": "实验性", "xpack.dataVisualizer.file.explanationFlyout.closeButton": "关闭", "xpack.dataVisualizer.file.explanationFlyout.content": "产生分析结果的逻辑步骤。", "xpack.dataVisualizer.file.explanationFlyout.title": "分析说明", @@ -8662,7 +8661,6 @@ "xpack.dataVisualizer.file.importSummary.indexPatternTitle": "索引模式", "xpack.dataVisualizer.file.importSummary.indexTitle": "索引", "xpack.dataVisualizer.file.importSummary.ingestPipelineTitle": "采集管道", - "xpack.dataVisualizer.file.importView.experimentalFeatureTooltip": "实验性功能。我们很乐意听取您的反馈意见。", "xpack.dataVisualizer.file.importView.importButtonLabel": "导入", "xpack.dataVisualizer.file.importView.importDataTitle": "导入数据", "xpack.dataVisualizer.file.importView.importPermissionError": "您无权创建或将数据导入索引 {index}", @@ -8697,14 +8695,11 @@ "xpack.dataVisualizer.file.simpleImportSettings.indexNameFormRowLabel": "索引名称", "xpack.dataVisualizer.file.simpleImportSettings.indexNamePlaceholder": "索引名称", "xpack.dataVisualizer.file.welcomeContent.delimitedTextFilesDescription": "分隔的文本文件,例如 CSV 和 TSV", - "xpack.dataVisualizer.file.welcomeContent.experimentalFeatureDescription": "此功能为实验性功能。有反馈?如欲提供反馈,请在 {githubLink} 中创建问题。", - "xpack.dataVisualizer.file.welcomeContent.experimentalFeatureTooltip": "实验性功能。我们很乐意听取您的反馈意见。", "xpack.dataVisualizer.file.welcomeContent.logFilesWithCommonFormatDescription": "具有时间戳通用格式的日志文件", "xpack.dataVisualizer.file.welcomeContent.newlineDelimitedJsonDescription": "换行符分隔的 JSON", "xpack.dataVisualizer.file.welcomeContent.supportedFileFormatDescription": "File Data Visualizer 支持以下文件格式:", "xpack.dataVisualizer.file.welcomeContent.uploadedFilesAllowedSizeDescription": "您可以上传不超过 {maxFileSize} 的文件。", "xpack.dataVisualizer.file.welcomeContent.visualizeDataFromLogFileDescription": "File Data Visualizer 可帮助您理解日志文件中的字段和指标。上传文件、分析文件数据,然后选择是否将数据导入 Elasticsearch 索引。", - "xpack.dataVisualizer.file.welcomeContent.visualizeDataFromLogFileTitle": "可视化来自日志文件的数据 {experimentalBadge}", "xpack.dataVisualizer.index.actionsPanel.discoverAppTitle": "Discover", "xpack.dataVisualizer.index.actionsPanel.exploreTitle": "浏览您的数据", "xpack.dataVisualizer.index.actionsPanel.viewIndexInDiscoverDescription": "浏览您的索引中的文档。", @@ -14534,8 +14529,6 @@ "xpack.ml.dataVisualizer.fileBasedLabel": "文件", "xpack.ml.datavisualizer.selector.dataVisualizerDescription": "Machine Learning 数据可视化工具通过分析日志文件或现有 Elasticsearch 索引中的指标和字段,帮助您理解数据。", "xpack.ml.datavisualizer.selector.dataVisualizerTitle": "数据可视化工具", - "xpack.ml.datavisualizer.selector.experimentalBadgeLabel": "实验性", - "xpack.ml.datavisualizer.selector.experimentalBadgeTooltipLabel": "实验性功能。我们很乐意听取您的反馈意见。", "xpack.ml.datavisualizer.selector.importDataDescription": "从日志文件导入数据。您可以上传不超过 {maxFileSize} 的文件。", "xpack.ml.datavisualizer.selector.importDataTitle": "导入数据", "xpack.ml.datavisualizer.selector.selectIndexButtonLabel": "选择索引模式", @@ -21150,7 +21143,6 @@ "xpack.securitySolution.rowRenderer.wasPreventedFromExecutingAMaliciousProcessDescription": "被阻止执行恶意进程", "xpack.securitySolution.rowRenderer.wasPreventedFromModifyingAMaliciousFileDescription": "被阻止修改恶意文件", "xpack.securitySolution.rowRenderer.wasPreventedFromRenamingAMaliciousFileDescription": "被阻止重命名恶意文件", - "xpack.securitySolution.search.administration": "终端管理", "xpack.securitySolution.search.administration.trustedApps": "受信任的应用程序", "xpack.securitySolution.search.cases": "案例", "xpack.securitySolution.search.cases.configure": "配置案例", @@ -23967,7 +23959,6 @@ "xpack.uptime.monitorDetails.title.pingType.icmp": "ICMP ping", "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "响应异常分数", - "xpack.uptime.monitorList.defineConnector.description": "要开始启用告警,请在以下位置定义默认告警操作连接器", "xpack.uptime.monitorList.downLineSeries.downLabel": "关闭检查", "xpack.uptime.monitorList.drawer.missingLocation": "某些 Heartbeat 实例未定义位置。{link}到您的 Heartbeat 配置。", "xpack.uptime.monitorList.drawer.mostRecentRun": "最新测试运行", @@ -24601,4 +24592,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 625406c0cb97bd..5aa3c80610d035 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -15,7 +15,7 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml"], + "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"], "owner": { "name": "Uptime", "githubTeam": "uptime" diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 63d32948388d73..869ecda3d29cf2 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -42,6 +42,7 @@ import { LazySyntheticsPolicyCreateExtension, LazySyntheticsPolicyEditExtension, } from '../components/fleet_package'; +import { LazySyntheticsCustomAssetsExtension } from '../components/fleet_package/lazy_synthetics_custom_assets_extension'; export interface ClientPluginsSetup { data: DataPublicPluginSetup; @@ -196,13 +197,19 @@ export class UptimePlugin registerExtension({ package: 'synthetics', view: 'package-policy-create', - component: LazySyntheticsPolicyCreateExtension, + Component: LazySyntheticsPolicyCreateExtension, }); registerExtension({ package: 'synthetics', view: 'package-policy-edit', - component: LazySyntheticsPolicyEditExtension, + Component: LazySyntheticsPolicyEditExtension, + }); + + registerExtension({ + package: 'synthetics', + view: 'package-detail-assets', + Component: LazySyntheticsCustomAssetsExtension, }); } } diff --git a/x-pack/plugins/uptime/public/components/common/higher_order/__snapshots__/responsive_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/higher_order/__snapshots__/responsive_wrapper.test.tsx.snap deleted file mode 100644 index 65b6d7cc39e558..00000000000000 --- a/x-pack/plugins/uptime/public/components/common/higher_order/__snapshots__/responsive_wrapper.test.tsx.snap +++ /dev/null @@ -1,221 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ResponsiveWrapper HOC is not responsive when prop is false 1`] = ` -<EuiPanel - paddingSize="m" -> - <Component - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - /> -</EuiPanel> -`; - -exports[`ResponsiveWrapper HOC renders a responsive wrapper 1`] = ` -<styled.div> - <Component - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } - /> -</styled.div> -`; diff --git a/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.test.tsx index 5a3dca171b206e..db254fcb56081d 100644 --- a/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { render } from '../../../lib/helper/rtl_helpers'; import { withResponsiveWrapper } from './responsive_wrapper'; interface Prop { @@ -20,12 +20,12 @@ describe('ResponsiveWrapper HOC', () => { }); it('renders a responsive wrapper', () => { - const component = shallowWithIntl(<WrappedByHOC isResponsive={true} />); - expect(component).toMatchSnapshot(); + const { getByTestId } = render(<WrappedByHOC isResponsive={true} />); + expect(getByTestId('uptimeWithResponsiveWrapper--wrapper')).toBeInTheDocument(); }); it('is not responsive when prop is false', () => { - const component = shallowWithIntl(<WrappedByHOC isResponsive={false} />); - expect(component).toMatchSnapshot(); + const { getByTestId } = render(<WrappedByHOC isResponsive={false} />); + expect(getByTestId('uptimeWithResponsiveWrapper--panel')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx index 6802682db5f56c..0e33cc3e38f031 100644 --- a/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx @@ -32,11 +32,11 @@ export const withResponsiveWrapper = <P extends {} & ResponsiveWrapperProps>( Component: FC<P> ): FC<ResponsiveWrapperProps & P> => ({ isResponsive, ...rest }: ResponsiveWrapperProps) => isResponsive ? ( - <ResponsiveWrapper> + <ResponsiveWrapper data-test-subj="uptimeWithResponsiveWrapper--wrapper"> <Component {...(rest as P)} /> </ResponsiveWrapper> ) : ( - <EuiPanel paddingSize="m"> + <EuiPanel paddingSize="m" hasBorder data-test-subj="uptimeWithResponsiveWrapper--panel"> <Component {...(rest as P)} /> </EuiPanel> ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/lazy_synthetics_custom_assets_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/lazy_synthetics_custom_assets_extension.tsx new file mode 100644 index 00000000000000..c2fe52d9004dfe --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/lazy_synthetics_custom_assets_extension.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +export const LazySyntheticsCustomAssetsExtension = lazy(async () => { + const { SyntheticsCustomAssetsExtension } = await import('./synthetics_custom_assets_extension'); + + return { + default: SyntheticsCustomAssetsExtension, + }; +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_custom_assets_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_custom_assets_extension.tsx new file mode 100644 index 00000000000000..7eda6efa205300 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_custom_assets_extension.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + PackageAssetsComponent, + CustomAssetsAccordionProps, + CustomAssetsAccordion, +} from '../../../../fleet/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from '../../apps/plugin'; +import { PLUGIN } from '../../../common/constants/plugin'; + +export const SyntheticsCustomAssetsExtension: PackageAssetsComponent = () => { + const { http } = useKibana<ClientPluginsStart>().services; + const views: CustomAssetsAccordionProps['views'] = [ + { + name: i18n.translate('xpack.uptime.fleetIntegration.assets.name', { + defaultMessage: 'Monitors', + }), + url: http?.basePath.prepend(`/app/${PLUGIN.ID}`) ?? '', + description: i18n.translate('xpack.uptime.fleetIntegration.assets.description', { + defaultMessage: 'View monitors in Uptime', + }), + }, + ]; + + return <CustomAssetsAccordion views={views} initialIsOpen />; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx index 86602a064b9d43..9ce5a509bdd520 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx @@ -34,7 +34,7 @@ export const MonitorDurationComponent = ({ hasMLJob, }: DurationChartProps) => { return ( - <EuiPanel paddingSize="m"> + <EuiPanel paddingSize="m" hasBorder> <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> <EuiFlexItem> <EuiTitle size="s"> diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx index 8cb1c49cbd9743..2112af06536695 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx @@ -89,17 +89,9 @@ export const MonitorPageTitle: React.FC = () => { return ( <> - <EuiFlexGroup wrap={false} data-test-subj="monitorTitle"> - <EuiFlexItem grow={false}> - <EuiTitle> - <h1 className="eui-textNoWrap">{nameOrId}</h1> - </EuiTitle> - <EuiSpacer size="xs" /> - </EuiFlexItem> - <EuiFlexItem grow={false} style={{ justifyContent: 'center' }}> - <EnableMonitorAlert monitorId={monitorId} selectedMonitor={selectedMonitor!} /> - </EuiFlexItem> - </EuiFlexGroup> + <EuiTitle> + <h1 className="eui-textNoWrap">{nameOrId}</h1> + </EuiTitle> <EuiSpacer size="s" /> <EuiFlexGroup wrap={false} gutterSize="s" alignItems="center"> <EuiFlexItem grow={false}> @@ -126,6 +118,7 @@ export const MonitorPageTitle: React.FC = () => { </EuiFlexItem> )} </EuiFlexGroup> + <EnableMonitorAlert monitorId={monitorId} selectedMonitor={selectedMonitor!} /> </> ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index b9ad176b8ed761..06c7ab7bff843c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -251,7 +251,7 @@ export const PingList = () => { }; return ( - <EuiPanel> + <EuiPanel hasBorder> <PingListHeader /> <EuiSpacer size="s" /> <EuiBasicTable diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.test.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.test.ts new file mode 100644 index 00000000000000..82e7dfd2d0ced9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderMonitorType } from './status_bar'; + +describe('StatusBar component', () => { + describe('renderMonitorType', () => { + it('handles http type', () => { + expect(renderMonitorType('http')).toBe('HTTP'); + }); + + it('handles tcp type', () => { + expect(renderMonitorType('tcp')).toBe('TCP'); + }); + + it('handles icmp type', () => { + expect(renderMonitorType('icmp')).toBe('ICMP'); + }); + + it('handles browser type', () => { + expect(renderMonitorType('browser')).toBe('Browser'); + }); + + it('returns empty string for `undefined`', () => { + expect(renderMonitorType(undefined)).toBe(''); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx index 18049a9de5c5c3..e8374d3792bfe5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx @@ -15,6 +15,7 @@ import { EuiDescriptionListDescription, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { MonitorSSLCertificate } from './ssl_certificate'; import * as labels from '../translations'; import { StatusByLocations } from './status_by_location'; @@ -40,6 +41,29 @@ export const MonListDescription = styled(EuiDescriptionListDescription)` } `; +export const renderMonitorType = (type: string | undefined) => { + switch (type) { + case 'http': + return i18n.translate('xpack.uptime.monitorDetails.statusBar.pingType.http', { + defaultMessage: 'HTTP', + }); + case 'tcp': + return i18n.translate('xpack.uptime.monitorDetails.statusBar.pingType.tcp', { + defaultMessage: 'TCP', + }); + case 'icmp': + return i18n.translate('xpack.uptime.monitorDetails.statusBar.pingType.icmp', { + defaultMessage: 'ICMP', + }); + case 'browser': + return i18n.translate('xpack.uptime.monitorDetails.statusBar.pingType.browser', { + defaultMessage: 'Browser', + }); + default: + return ''; + } +}; + export const MonitorStatusBar: React.FC = () => { const { monitorId, monitorStatus, monitorLocations = {} } = useStatusBar(); @@ -77,7 +101,7 @@ export const MonitorStatusBar: React.FC = () => { <> <MonListTitle aria-label={labels.typeAriaLabel}>{labels.typeLabel}</MonListTitle> <MonListDescription data-test-subj="monitor-page-type"> - {monitorStatus.monitor.type} + {renderMonitorType(monitorStatus?.monitor?.type)} </MonListDescription> </> )} diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx index 517bfbe594f08f..5b20e83f0ec85e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_details.tsx @@ -49,7 +49,7 @@ export const MonitorStatusDetailsComponent = ({ monitorLocations }: MonitorStatu }, []); return ( - <EuiPanel> + <EuiPanel hasBorder> <EuiFlexGroup gutterSize="l" wrap={true} responsive={true}> <EuiFlexItem grow={1}> <MonitorStatusBar /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index df8f5dff59dc22..610107f406306a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; @@ -104,7 +104,7 @@ export const StepDetailContainer: React.FC<Props> = ({ checkGroup, stepIndex }) : [], }} > - <EuiPanel> + <> {(!journey || journey.loading) && ( <EuiFlexGroup justifyContent="center"> <EuiFlexItem grow={false}> @@ -124,7 +124,7 @@ export const StepDetailContainer: React.FC<Props> = ({ checkGroup, stepIndex }) {journey && activeStep && !journey.loading && ( <WaterfallChartContainer checkGroup={checkGroup} stepIndex={stepIndex} /> )} - </EuiPanel> + </> </PageTemplateComponent> ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/__snapshots__/data_or_index_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__snapshots__/data_or_index_missing.test.tsx.snap deleted file mode 100644 index 45e40f71c0fdef..00000000000000 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/__snapshots__/data_or_index_missing.test.tsx.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DataOrIndexMissing component renders headingMessage 1`] = ` -<EuiFlexGroup - data-test-subj="data-missing" - justifyContent="center" -> - <EuiFlexItem - grow={false} - style={ - Object { - "flexBasis": 700, - } - } - > - <EuiSpacer - size="m" - /> - <EuiPanel> - <EuiEmptyPrompt - actions={ - <EuiFlexGroup> - <EuiFlexItem> - <EuiButton - color="primary" - fill={true} - href="/app/home#/tutorial/uptimeMonitors" - > - <FormattedMessage - defaultMessage="View setup instructions" - id="xpack.uptime.emptyState.viewSetupInstructions" - values={Object {}} - /> - </EuiButton> - </EuiFlexItem> - <EuiFlexItem> - <EuiButton - color="primary" - href="/app/uptime/settings" - > - <FormattedMessage - defaultMessage="Update index pattern settings" - id="xpack.uptime.emptyState.updateIndexPattern" - values={Object {}} - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - } - body={ - <React.Fragment> - <p> - <FormattedMessage - defaultMessage="Set up Heartbeat to start monitoring your services." - id="xpack.uptime.emptyState.configureHeartbeatToGetStartedMessage" - values={Object {}} - /> - </p> - <p> - <FormattedMessage - defaultMessage="If Heartbeat is already set up, confirm it's sending data to Elasticsearch, then update the index pattern settings to match the Heartbeat config." - id="xpack.uptime.emptyState.configureHeartbeatIndexSettings" - values={Object {}} - /> - </p> - </React.Fragment> - } - iconType="logoUptime" - title={ - <EuiTitle - size="l" - > - <h3> - <FormattedMessage - defaultMessage="Uptime index {indexName} not found" - id="xpack.uptime.emptyState.noIndexTitle" - values={ - Object { - "indexName": <em> - heartbeat-* - </em>, - } - } - /> - </h3> - </EuiTitle> - } - /> - </EuiPanel> - </EuiFlexItem> -</EuiFlexGroup> -`; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx index c6898971a693eb..caff055ce987ca 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.test.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; +import { screen } from '@testing-library/react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { render } from '../../../lib/helper/rtl_helpers'; import { DataOrIndexMissing } from './data_or_index_missing'; describe('DataOrIndexMissing component', () => { @@ -19,7 +20,7 @@ describe('DataOrIndexMissing component', () => { values={{ indexName: <em>heartbeat-*</em> }} /> ); - const component = shallowWithIntl(<DataOrIndexMissing headingMessage={headingMessage} />); - expect(component).toMatchSnapshot(); + render(<DataOrIndexMissing headingMessage={headingMessage} />); + expect(screen.getByText(/heartbeat-*/)).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx index 7f9839ff94dbe8..44e55de990bbf6 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx @@ -30,7 +30,7 @@ export const DataOrIndexMissing = ({ headingMessage, settings }: DataMissingProp <EuiFlexGroup justifyContent="center" data-test-subj="data-missing"> <EuiFlexItem grow={false} style={{ flexBasis: 700 }}> <EuiSpacer size="m" /> - <EuiPanel> + <EuiPanel hasBorder> <EuiEmptyPrompt iconType="logoUptime" title={ diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index 41a5e1f6f7c009..3bf6c4f4bcb14b 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -22,7 +22,7 @@ export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { return ( <EuiFlexGroup justifyContent="center"> <EuiFlexItem grow={false}> - <EuiPanel> + <EuiPanel hasBorder> <EuiEmptyPrompt iconType="securityApp" iconColor="subdued" diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap index cfdf7afba4e85e..a4fcb141d454b9 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap @@ -845,7 +845,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` } <div - class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + class="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive c0" @@ -1299,28 +1299,37 @@ exports[`MonitorList component renders the monitor list 1`] = ` class="euiPopover__anchor" > <div - class="euiSwitch euiSwitch--compressed" + class="euiFormRow" + id="generated-id-row" > - <button - aria-checked="false" - aria-label="Enable rule" - class="euiSwitch__button" - data-test-subj="uptimeDisplayDefineConnector" - id="defineAlertSettingsSwitch" - role="switch" - type="button" + <div + class="euiFormRow__fieldWrapper" > - <span - class="euiSwitch__body" + <div + class="euiSwitch euiSwitch--compressed" > - <span - class="euiSwitch__thumb" - /> - <span - class="euiSwitch__track" - /> - </span> - </button> + <button + aria-checked="false" + aria-label="Enable status alerts" + class="euiSwitch__button" + data-test-subj="uptimeDisplayDefineConnector" + id="generated-id" + role="switch" + type="button" + > + <span + class="euiSwitch__body" + > + <span + class="euiSwitch__thumb" + /> + <span + class="euiSwitch__track" + /> + </span> + </button> + </div> + </div> </div> </div> </div> @@ -1552,28 +1561,37 @@ exports[`MonitorList component renders the monitor list 1`] = ` class="euiPopover__anchor" > <div - class="euiSwitch euiSwitch--compressed" + class="euiFormRow" + id="generated-id-row" > - <button - aria-checked="false" - aria-label="Enable rule" - class="euiSwitch__button" - data-test-subj="uptimeDisplayDefineConnector" - id="defineAlertSettingsSwitch" - role="switch" - type="button" + <div + class="euiFormRow__fieldWrapper" > - <span - class="euiSwitch__body" + <div + class="euiSwitch euiSwitch--compressed" > - <span - class="euiSwitch__thumb" - /> - <span - class="euiSwitch__track" - /> - </span> - </button> + <button + aria-checked="false" + aria-label="Enable status alerts" + class="euiSwitch__button" + data-test-subj="uptimeDisplayDefineConnector" + id="generated-id" + role="switch" + type="button" + > + <span + class="euiSwitch__body" + > + <span + class="euiSwitch__thumb" + /> + <span + class="euiSwitch__track" + /> + </span> + </button> + </div> + </div> </div> </div> </div> diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.test.tsx new file mode 100644 index 00000000000000..aa257177970a18 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { DefineAlertConnectors } from './define_connectors'; +import { screen } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { ENABLE_STATUS_ALERT } from './translations'; +import { render } from '../../../../lib/helper/rtl_helpers'; + +describe('EnableAlertComponent', () => { + it('does not showHelpText or render popover when showHelpText and renderPopOver are false', () => { + render(<DefineAlertConnectors />); + expect(screen.getByTestId('uptimeDisplayDefineConnector')).toBeInTheDocument(); + expect(screen.queryByText(ENABLE_STATUS_ALERT)).not.toBeInTheDocument(); + expect(screen.queryByText(/Define a default connector/)).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('uptimeDisplayDefineConnector')); + + expect(screen.queryByTestId('uptimeSettingsDefineConnector')).not.toBeInTheDocument(); + }); + + it('shows label when showLabel is true', () => { + render(<DefineAlertConnectors showLabel />); + expect(screen.getByText(ENABLE_STATUS_ALERT)).toBeInTheDocument(); + }); + + it('shows helpText when showHelpText is true', () => { + render(<DefineAlertConnectors showHelpText />); + expect(screen.getByText(/Define a default connector/)).toBeInTheDocument(); + }); + + it('renders popover on click when showPopover is true', () => { + render(<DefineAlertConnectors showPopover />); + + fireEvent.click(screen.getByTestId('uptimeDisplayDefineConnector')); + + expect(screen.getByTestId('uptimeSettingsDefineConnector')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx index 956e18f3d7cc34..dcc70c16e920be 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/define_connectors.tsx @@ -6,41 +6,72 @@ */ import React, { useState } from 'react'; -import { EuiPopover, EuiSwitch, EuiText } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; +import { EuiSwitch, EuiPopover, EuiText, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ReactRouterEuiLink } from '../../../common/react_router_helpers'; -import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../common/constants'; +import { SETTINGS_ROUTE } from '../../../../../common/constants'; import { ENABLE_STATUS_ALERT } from './translations'; -const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.defineConnector', { - defaultMessage: 'Define a default connector', -}); +interface Props { + showPopover?: boolean; + showHelpText?: boolean; + showLabel?: boolean; +} -export const DefineAlertConnectors = () => { +export const DefineAlertConnectors = ({ + showPopover = false, + showHelpText = false, + showLabel = false, +}: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onButtonClick = () => setIsPopoverOpen((val) => !val); const closePopover = () => setIsPopoverOpen(false); - const isMonitorPage = useRouteMatch(MONITOR_ROUTE); - return ( <EuiPopover button={ - <EuiSwitch - id={'defineAlertSettingsSwitch'} - label={ENABLE_STATUS_ALERT} - showLabel={!!isMonitorPage} - aria-label={ENABLE_STATUS_ALERT} - onChange={onButtonClick} - checked={false} - compressed={!isMonitorPage} - data-test-subj={'uptimeDisplayDefineConnector'} - /> + <> + <EuiFormRow + helpText={ + showHelpText ? ( + <FormattedMessage + id="xpack.uptime.monitorList.defineConnector.description" + defaultMessage="Define a default connector in {link} to enable monitor status alerts." + values={{ + link: ( + <ReactRouterEuiLink + to={SETTINGS_ROUTE + '?focusConnectorField=true'} + data-test-subj={'uptimeSettingsLink'} + target="_blank" + > + <FormattedMessage + id="xpack.uptime.page_header.defineConnector.settingsLink" + defaultMessage="Settings" + /> + </ReactRouterEuiLink> + ), + }} + /> + ) : undefined + } + > + <EuiSwitch + id={'defineAlertSettingsSwitch'} + label={ENABLE_STATUS_ALERT} + showLabel={showLabel} + aria-label={ENABLE_STATUS_ALERT} + // this switch is read only, no onChange applied + onChange={showPopover ? onButtonClick : () => {}} + checked={false} + compressed={true} + disabled={!showPopover} + data-test-subj={'uptimeDisplayDefineConnector'} + /> + </EuiFormRow> + </> } - isOpen={isPopoverOpen} + isOpen={showPopover ? isPopoverOpen : false} closePopover={closePopover} > <EuiText style={{ width: '350px' }} data-test-subj={'uptimeSettingsDefineConnector'}> @@ -48,10 +79,13 @@ export const DefineAlertConnectors = () => { to={SETTINGS_ROUTE + '?focusConnectorField=true'} data-test-subj={'uptimeSettingsLink'} > - {SETTINGS_LINK_TEXT} + <FormattedMessage + id="xpack.uptime.page_header.defineConnector.popover.defaultLink" + defaultMessage="Define a default connector" + /> </ReactRouterEuiLink>{' '} <FormattedMessage - id="xpack.uptime.monitorList.defineConnector.description" + id="xpack.uptime.monitorList.defineConnector.popover.description" defaultMessage="to receive status alerts." /> </EuiText> diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx index 427985839ba892..53ba1b8ed57eb5 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx @@ -18,7 +18,7 @@ import { AlertsResult } from '../../../../state/actions/types'; describe('EnableAlertComponent', () => { it('it displays define connectors when there is none', () => { - const { getByTestId, getByLabelText, getByText } = render( + const { getByTestId, getByLabelText, getByText, getByRole } = render( <EnableMonitorAlert monitorId={'testMonitor'} selectedMonitor={makePing({ name: 'My website' })} @@ -29,11 +29,12 @@ describe('EnableAlertComponent', () => { fireEvent.click(getByTestId('uptimeDisplayDefineConnector')); - expect(getByTestId('uptimeSettingsLink')).toHaveAttribute( + expect(getByRole('link', { name: 'Define a default connector' })).toHaveAttribute( 'href', '/settings?focusConnectorField=true' ); - expect(getByText('to receive status alerts.')); + expect(getByText(/Define a default connector/)); + expect(getByText(/to receive status alerts./)); }); it('does not displays define connectors when there is connector', () => { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx index f0f7c4d91c4f5d..9db6c3b4b0acb8 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx @@ -125,6 +125,10 @@ export const EnableMonitorAlert = ({ monitorId, selectedMonitor }: Props) => { </EuiToolTip> </div> ) : ( - <DefineAlertConnectors /> + <DefineAlertConnectors + showPopover={!isMonitorPage} + showHelpText={!!isMonitorPage} + showLabel={!!isMonitorPage} + /> ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts index eadf8febebcf26..ce6708abf9ade8 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts @@ -8,11 +8,11 @@ import { i18n } from '@kbn/i18n'; export const ENABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.enableDownAlert', { - defaultMessage: 'Enable rule', + defaultMessage: 'Enable status alerts', }); export const DISABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.disableDownAlert', { - defaultMessage: 'Disable rule', + defaultMessage: 'Disable status alerts', }); export const EXPAND_TAGS_LABEL = i18n.translate('xpack.uptime.monitorList.tags.expand', { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 9a0054f77252e1..0b707588acfb56 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -173,7 +173,7 @@ export const MonitorListComponent: ({ ]; return ( - <EuiPanel> + <EuiPanel hasBorder> <MonitorListHeader /> <EuiSpacer size="m" /> <EuiBasicTable diff --git a/x-pack/plugins/uptime/public/components/overview/status_panel.tsx b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx index 6faa56bb358fbc..45e0ecaf9fc462 100644 --- a/x-pack/plugins/uptime/public/components/overview/status_panel.tsx +++ b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx @@ -13,7 +13,7 @@ import { SnapshotComponent } from './snapshot'; const STATUS_CHART_HEIGHT = '160px'; export const StatusPanel = ({}) => ( - <EuiPanel> + <EuiPanel hasBorder> <EuiFlexGroup gutterSize="l"> <EuiFlexItem grow={2}> <SnapshotComponent height={STATUS_CHART_HEIGHT} /> diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx index 47b89e82dc5c7c..0da6f034e53bbf 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx @@ -5,13 +5,7 @@ * 2.0. */ -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiButtonIcon, - EuiPanel, - EuiTitle, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; import styled from 'styled-components'; @@ -147,7 +141,7 @@ export const StepsList = ({ data, error, loading }: Props) => { }; return ( - <EuiPanel> + <> <EuiTitle> <h2> {statusMessage( @@ -176,6 +170,6 @@ export const StepsList = ({ data, error, loading }: Props) => { tableLayout={'auto'} rowProps={getRowProps} /> - </EuiPanel> + </> ); }; diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index 5f2699240425a6..88bae5536c05f4 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -13,7 +13,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, - EuiPanel, EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -148,7 +147,7 @@ export const SettingsPage: React.FC = () => { ); return ( - <EuiPanel style={{ maxWidth: 1000, margin: 'auto' }}> + <> <EuiFlexGroup> <EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem> </EuiFlexGroup> @@ -213,6 +212,6 @@ export const SettingsPage: React.FC = () => { </div> </EuiFlexItem> </EuiFlexGroup> - </EuiPanel> + </> ); }; diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index d0e17476530b7e..25b1ef97d30875 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -177,12 +177,19 @@ export default function ({ getService }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/64473 - describe.skip('list', function () { + describe('list', function () { this.tags(['skipCloud']); it('should list all the indices with the expected properties and data enrichers', async function () { - const { body } = await list().expect(200); + // Create an index that we can assert against + await createIndex('test_index'); + + // Verify indices request + const { body: indices } = await list().expect(200); + + // Find the "test_index" created to verify expected keys + const indexCreated = indices.find((index) => index.name === 'test_index'); + const expectedKeys = [ 'health', 'hidden', @@ -203,7 +210,8 @@ export default function ({ getService }) { // We need to sort the keys before comparing then, because race conditions // can cause enrichers to register in non-deterministic order. const sortedExpectedKeys = expectedKeys.sort(); - const sortedReceivedKeys = Object.keys(body[0]).sort(); + const sortedReceivedKeys = Object.keys(indexCreated).sort(); + expect(sortedReceivedKeys).to.eql(sortedExpectedKeys); }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts index 37f7b09e8b7d21..9c687fc74acce1 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -286,13 +286,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(unfilteredServiceNames).to.eql(filteredServiceNames); - expect( - filteredResponse.body.items.every((item) => { - // make sure it did not query transaction data - return isEmpty(item.avgResponseTime); - }) - ).to.be(true); - expect(filteredResponse.body.items.every((item) => !!item.agentName)).to.be(true); }); } diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 2c180514059647..427d8c21635c4f 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const security = getService('security'); - describe('async search with scripted fields', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104362 + describe.skip('async search with scripted fields', function () { this.tags(['skipFirefox']); before(async function () { diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 3eb66204df5640..0b018b4428e1d0 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -73,7 +73,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Generate CSV: new search', () => { + // FLAKY: https://github.com/elastic/kibana/issues/104372 + describe.skip('Generate CSV: new search', () => { beforeEach(async () => { await kibanaServer.importExport.load(ecommerceSOPath); await PageObjects.common.navigateToApp('discover'); diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js index 3cc7d8e07d623f..67e6d7f93a4cd6 100644 --- a/x-pack/test/functional/apps/maps/blended_vector_layer.js +++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js @@ -29,7 +29,7 @@ export default function ({ getPageObjects, getService }) { it('should request documents when zoomed to smaller regions showing less data', async () => { const { rawResponse: response } = await PageObjects.maps.getResponse(); // Allow a range of hits to account for variances in browser window size. - expect(response.hits.hits.length).to.be.within(35, 45); + expect(response.hits.hits.length).to.be.within(5, 12); }); it('should request clusters when zoomed to larger regions showing lots of data', async () => { diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 4da36a44cff08a..de2233fdf791dc 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -53,7 +53,7 @@ export default function ({ getPageObjects, getService }) { describe('inspector', () => { it('should register elasticsearch request in inspector', async () => { const hits = await PageObjects.maps.getHits(); - expect(hits).to.equal('6'); + expect(hits).to.equal('5'); }); }); @@ -74,7 +74,7 @@ export default function ({ getPageObjects, getService }) { const requestStats = await inspector.getTableData(); const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); await inspector.close(); - expect(hits).to.equal('3'); + expect(hits).to.equal('2'); }); it('should re-fetch query when "refresh" is clicked', async () => { diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/es_geo_grid_source.js index 27949ca720e34a..26eabafef7d1fc 100644 --- a/x-pack/test/functional/apps/maps/es_geo_grid_source.js +++ b/x-pack/test/functional/apps/maps/es_geo_grid_source.js @@ -59,9 +59,9 @@ export default function ({ getPageObjects, getService }) { }); it('should not rerequest when zoom changes do not cause geotile_grid precision to change', async () => { - await PageObjects.maps.setView(DATA_CENTER_LAT, DATA_CENTER_LON, 1.2); + await PageObjects.maps.setView(DATA_CENTER_LAT, DATA_CENTER_LON, 1.4); const beforeSameZoom = await getRequestTimestamp(); - await PageObjects.maps.setView(DATA_CENTER_LAT, DATA_CENTER_LON, 1.8); + await PageObjects.maps.setView(DATA_CENTER_LAT, DATA_CENTER_LON, 1.6); const afterTimestamp = await getRequestTimestamp(); expect(afterTimestamp).to.equal(beforeSameZoom); }); diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js index 52cfa44923f637..cf6051cde8be7a 100644 --- a/x-pack/test/functional/apps/maps/layer_visibility.js +++ b/x-pack/test/functional/apps/maps/layer_visibility.js @@ -32,7 +32,7 @@ export default function ({ getPageObjects, getService }) { it('should fetch layer data when layer is made visible', async () => { await PageObjects.maps.toggleLayerVisibility('logstash'); const hits = await PageObjects.maps.getHits(); - expect(hits).to.equal('6'); + expect(hits).to.equal('5'); }); }); } diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js index bdc9011ec2cba6..3392e418bd987f 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }) { const requestStats = await inspector.getTableData(); const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); await inspector.close(); - expect(hits).to.equal('2'); + expect(hits).to.equal('1'); }); it('should override query stored with map when query is provided in app state', async () => { @@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }) { const requestStats = await inspector.getTableData(); const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); await inspector.close(); - expect(hits).to.equal('2'); + expect(hits).to.equal('1'); }); }); }); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index 3867ed6f7dfea0..dee5b5a5e31c0b 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -273,6 +273,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('creates filebeat config'); await ml.dataVisualizerFileBased.selectCreateFilebeatConfig(); + + await ml.testExecution.logTestStep('closes filebeat config'); + await ml.dataVisualizerFileBased.closeCreateFilebeatConfig(); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts index 93e3b67ca15650..54d7b6ac294d15 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -15,16 +15,21 @@ export default function ({ getService }: FtrProviderContext) { this.tags(['mlqa']); const indexPatternName = 'ft_farequote'; - // @TODO: Re-enable in follow up - // const advancedJobWizardDatafeedQuery = `{ - // "bool": { - // "must": [ - // { - // "match_all": {} - // } - // ] - // } - // }`; // Note query is not currently passed to the wizard + + const advancedJobWizardDatafeedQuery = JSON.stringify( + { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + null, + 2 + ); + // Note query is not currently passed to the wizard before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); @@ -48,20 +53,19 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(indexPatternName); }); - // @TODO: Re-enable in follow up - // it('opens the advanced job wizard', async () => { - // await ml.testExecution.logTestStep('displays the actions panel with advanced job card'); - // await ml.dataVisualizerIndexBased.assertActionsPanelExists(); - // await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); - // await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardExists(); - // - // // Note the search is not currently passed to the wizard, just the index. - // await ml.testExecution.logTestStep('displays the actions panel with advanced job card'); - // await ml.dataVisualizerIndexBased.clickCreateAdvancedJobButton(); - // await ml.jobTypeSelection.assertAdvancedJobWizardOpen(); - // await ml.jobWizardAdvanced.assertDatafeedQueryEditorExists(); - // await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(advancedJobWizardDatafeedQuery); - // }); + it('opens the advanced job wizard', async () => { + await ml.testExecution.logTestStep('displays the actions panel with advanced job card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); + await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardExists(); + + // Note the search is not currently passed to the wizard, just the index. + await ml.testExecution.logTestStep('displays the actions panel with advanced job card'); + await ml.dataVisualizerIndexBased.clickCreateAdvancedJobButton(); + await ml.jobTypeSelection.assertAdvancedJobWizardOpen(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorExists(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(advancedJobWizardDatafeedQuery); + }); }); describe('view in discover page action', function () { diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 18f4f6a38a7b10..10b57de911a10e 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -93,8 +93,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - // @TODO: Re-enable in follow up - // const ecExpectedModuleId = 'sample_data_ecommerce'; + + const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -365,11 +365,10 @@ export default function ({ getService }: FtrProviderContext) { } await ml.dataVisualizerIndexBased.assertViewInDiscoverCard(testUser.discoverAvailable); - // @TODO: Re-enable in follow up - // await ml.testExecution.logTestStep('should display job cards'); - // await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); - // await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); - // await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardExists(); + await ml.testExecution.logTestStep('should display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); + await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); + await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardExists(); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index a53ed2fafe30c7..920d82ed381c0b 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -94,7 +94,6 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - // const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -357,11 +356,9 @@ export default function ({ getService }: FtrProviderContext) { } await ml.dataVisualizerIndexBased.assertViewInDiscoverCard(testUser.discoverAvailable); - // @TODO: Re-enable in follow up - // await ml.testExecution.logTestStep('should not display job cards'); - // await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); - // await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); - // await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); + await ml.testExecution.logTestStep('should not display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts index 242163d83c4567..5cee16c10e215e 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts +++ b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts @@ -46,6 +46,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); await ml.settingsCalendar.navigateToCalendarCreationPage(); + await ml.settingsCalendar.waitForFormEnabled(); + await ml.testExecution.logTestStep('calendar creation sets calendar to apply to all jobs'); await ml.settingsCalendar.toggleApplyToAllJobsSwitch(true); await ml.settingsCalendar.assertJobSelectionNotExists(); @@ -79,6 +81,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); await ml.settingsCalendar.navigateToCalendarCreationPage(); + await ml.settingsCalendar.waitForFormEnabled(); + await ml.testExecution.logTestStep( 'calendar creation verifies the job selection and job group section are displayed' ); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index e83eabfb05f44a..ff6103f16e4944 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects([ 'common', 'error', @@ -24,10 +25,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('feature controls security', () => { before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/timelion/feature_controls'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); }); + after(async () => { + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + }); + describe('global timelion all privileges', () => { before(async () => { await security.role.create('global_timelion_all_role', { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index 91c357f37085e7..a1dea695fce86a 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); + const kibanaServer = getService('kibanaServer'); describe('timelion', () => { before(async () => { @@ -23,29 +24,44 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load('x-pack/test/functional/es_archives/timelion/feature_controls'); + // await esArchiver.load('x-pack/test/functional/es_archives/timelion/feature_controls'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); + await spacesService.create({ id: 'custom_space', name: 'custom_space', disabledFeatures: [], }); + + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/timelion_custom_space.json', + { space: 'custom_space' } + ); }); after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload('x-pack/test/functional/es_archives/timelion/feature_controls'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); }); it('shows timelion navlink', async () => { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); expect(navLinks).to.contain('Timelion'); }); it(`allows a timelion sheet to be created`, async () => { - await PageObjects.common.navigateToApp('timelion'); + await PageObjects.common.navigateToApp('timelion', { + basePath: '/s/custom_space', + }); + await PageObjects.timelion.saveTimelionSheet(); }); }); @@ -54,17 +70,29 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load('x-pack/test/functional/es_archives/timelion/feature_controls'); + // await esArchiver.load('x-pack/test/functional/es_archives/timelion/feature_controls'); + + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); + await spacesService.create({ id: 'custom_space', name: 'custom_space', disabledFeatures: ['timelion'], }); + + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/timelion_custom_space.json', + { space: 'custom_space' } + ); }); after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload('x-pack/test/functional/es_archives/timelion/feature_controls'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json' + ); }); it(`doesn't show timelion navlink`, async () => { diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts index a4740de8e9a2b0..c4996299f0d43a 100644 --- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -253,7 +253,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); }); - it.skip('allows configuring http advanced options', async () => { + it('allows configuring http advanced options', async () => { // This test ensures that updates made to the Synthetics Policy are carried all the way through // to the generated Agent Policy that is dispatch down to the Elastic Agent. const config = generateHTTPConfig('http://elastic.co'); diff --git a/x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json b/x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json new file mode 100644 index 00000000000000..323dbb67d54b87 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/timelion/feature_controls.json @@ -0,0 +1,52 @@ +{ + "attributes": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "coreMigrationVersion": "7.14.0", + "id": "7.0.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_interval": "auto", + "timelion_rows": 2, + "timelion_sheet": [ + ".es(*)" + ], + "title": "i-exist", + "version": 1 + }, + "coreMigrationVersion": "7.14.0", + "id": "i-exist", + "references": [], + "type": "timelion-sheet", + "version": "WzYsMl0=" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kilobytes\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value / 1000\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"machine os raw\",\"type\":\"string\",\"count\":0,\"scripted\":true,\"script\":\"doc['machine.os.raw'].value\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.14.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzUsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/fixtures/kbn_archiver/timelion/timelion_custom_space.json b/x-pack/test/functional/fixtures/kbn_archiver/timelion/timelion_custom_space.json new file mode 100644 index 00000000000000..f27149f9d7eb65 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/timelion/timelion_custom_space.json @@ -0,0 +1,20 @@ +{ + "attributes": { + "description": "", + "hits": 0, + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_interval": "auto", + "timelion_rows": 2, + "timelion_sheet": [ + ".es(*).label('custom space sheet')" + ], + "title": "i-exist", + "version": 1 + }, + "coreMigrationVersion": "7.14.0", + "id": "i-exist", + "references": [], + "type": "timelion-sheet", + "version": "WzcsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts index 56a67d9e6fbd43..3321234a345e4a 100644 --- a/x-pack/test/functional/page_objects/synthetics_integration_page.ts +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -86,7 +86,7 @@ export function SyntheticsIntegrationPageProvider({ * @params {value} the value of the input */ async fillTextInputByTestSubj(testSubj: string, value: string) { - const field = await testSubjects.find(testSubj, 5000); + const field = await testSubjects.find(testSubj); await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight }); await field.click(); await field.clearValue(); @@ -118,7 +118,7 @@ export function SyntheticsIntegrationPageProvider({ */ async findHTTPAdvancedOptionsAccordion() { await this.ensureIsOnPackagePage(); - const accordion = await testSubjects.find('syntheticsHTTPAdvancedFieldsAccordion', 5000); + const accordion = await testSubjects.find('syntheticsHTTPAdvancedFieldsAccordion'); return accordion; }, diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 291e5a8964553f..783be207baf22b 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -139,5 +139,10 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.click('fileDataVisFilebeatConfigLink'); await testSubjects.existOrFail('fileDataVisFilebeatConfigPanel'); }, + + async closeCreateFilebeatConfig() { + await testSubjects.click('fileBeatConfigFlyoutCloseButton'); + await testSubjects.missingOrFail('fileDataVisFilebeatConfigPanel'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index b2fc121f99936c..8959e93623c1ce 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -214,6 +214,11 @@ export function MachineLearningSettingsCalendarProvider( ); }, + async waitForFormEnabled() { + // @ts-expect-error null is acceptable for a disabled attribute that no longer exists. + await testSubjects.waitForAttributeToChange('mlCalendarIdInput', 'disabled', null); + }, + async assertCalendarRowExists(calendarId: string) { await testSubjects.existOrFail(this.calendarRowSelector(calendarId)); }, diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index aff1402f5567ee..901744719144c2 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -25,7 +25,6 @@ export default function ({ getService }: FtrProviderContext) { describe(`(${testUser.user})`, function () { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - // const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -134,11 +133,9 @@ export default function ({ getService }: FtrProviderContext) { } await ml.dataVisualizerIndexBased.assertViewInDiscoverCard(testUser.discoverAvailable); - // @TODO: Re-enable in follow up - // await ml.testExecution.logTestStep('should not display job cards'); - // await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); - // await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); - // await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); + await ml.testExecution.logTestStep('should not display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index 2e5216d7225186..0f271719a0d0f2 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -25,8 +25,6 @@ export default function ({ getService }: FtrProviderContext) { describe(`(${testUser.user})`, function () { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - // @TODO: Re-enable in follow up - // const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -135,11 +133,9 @@ export default function ({ getService }: FtrProviderContext) { } await ml.dataVisualizerIndexBased.assertViewInDiscoverCard(testUser.discoverAvailable); - // @TODO: Re-enable in follow up - // await ml.testExecution.logTestStep('should not display job cards'); - // await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); - // await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); - // await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); + await ml.testExecution.logTestStep('should not display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + await ml.dataVisualizerIndexBased.assertCreateDataFrameAnalyticsCardNotExists(); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts new file mode 100644 index 00000000000000..a0f4a3f91fe32a --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../services/fixtures'; +import { FtrProviderContext } from '../ftr_provider_context'; + +import { ILM_POLICY_NAME } from '../../../plugins/reporting/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const supertestNoAuth = getService('supertestWithoutAuth'); + const reportingAPI = getService('reportingAPI'); + + describe('ILM policy migration APIs', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + }); + + afterEach(async () => { + await reportingAPI.deleteAllReports(); + await reportingAPI.migrateReportingIndices(); // ensure that the ILM policy exists + }); + + it('detects when no migration is needed', async () => { + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + + // try creating a report + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('detects when reporting indices should be migrated due to missing ILM policy', async () => { + await reportingAPI.makeAllReportingPoliciesUnmanaged(); + // TODO: Remove "any" when no longer through type issue "policy_id" missing + await es.ilm.deleteLifecycle({ policy: ILM_POLICY_NAME } as any); + + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('policy-not-found'); + // assert that migration fixes this + await reportingAPI.migrateReportingIndices(); + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('detects when reporting indices should be migrated due to unmanaged indices', async () => { + await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await supertestNoAuth + .post(`/api/reporting/generate/csv`) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('indices-not-managed-by-policy'); + // assert that migration fixes this + await reportingAPI.migrateReportingIndices(); + expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); + }); + + it('does not override an existing ILM policy', async () => { + const customLifecycle = { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + delete: { + min_age: '0ms', + actions: { + delete: { + delete_searchable_snapshot: true, + }, + }, + }, + }, + }, + }; + + // customize the lifecycle policy + await es.ilm.putLifecycle({ + policy: ILM_POLICY_NAME, + body: customLifecycle, + }); + + await reportingAPI.migrateReportingIndices(); + + const { + body: { + [ILM_POLICY_NAME]: { policy }, + }, + } = await es.ilm.getLifecycle({ policy: ILM_POLICY_NAME }); + + expect(policy).to.eql(customLifecycle.policy); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 15960e45d4a62a..fed842427ab90d 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting API Integration Tests with Security disabled', function () { this.tags('ciGroup13'); loadTestFile(require.resolve('./job_apis')); + loadTestFile(require.resolve('./ilm_migration_apis')); }); } diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index e45af4bd140b02..eb32de9d0dc9ce 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -164,6 +164,36 @@ export function createScenarios({ getService }: Pick<FtrProviderContext, 'getSer }); }; + const checkIlmMigrationStatus = async () => { + log.debug('ReportingAPI.checkIlmMigrationStatus'); + const { body } = await supertestWithoutAuth + .get('/api/reporting/ilm_policy_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + return body.status; + }; + + const migrateReportingIndices = async () => { + log.debug('ReportingAPI.migrateReportingIndices'); + await supertestWithoutAuth + .put('/api/reporting/deprecations/migrate_ilm_policy') + .set('kbn-xsrf', 'xxx') + .expect(200); + }; + + const makeAllReportingPoliciesUnmanaged = async () => { + log.debug('ReportingAPI.makeAllReportingPoliciesUnmanaged'); + const settings: any = { + 'index.lifecycle.name': null, + }; + await esSupertest + .put('/.reporting*/_settings') + .send({ + settings, + }) + .expect(200); + }; + return { initEcommerce, teardownEcommerce, @@ -182,5 +212,8 @@ export function createScenarios({ getService }: Pick<FtrProviderContext, 'getSer postJob, postJobJSON, deleteAllReports, + checkIlmMigrationStatus, + migrateReportingIndices, + makeAllReportingPoliciesUnmanaged, }; } diff --git a/x-pack/test/search_sessions_integration/config.ts b/x-pack/test/search_sessions_integration/config.ts index 1e2d648712098a..9dc542038a48a5 100644 --- a/x-pack/test/search_sessions_integration/config.ts +++ b/x-pack/test/search_sessions_integration/config.ts @@ -22,6 +22,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), + resolve(__dirname, './tests/apps/dashboard/session_sharing'), resolve(__dirname, './tests/apps/discover'), resolve(__dirname, './tests/apps/lens'), resolve(__dirname, './tests/apps/management/search_sessions'), diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts new file mode 100644 index 00000000000000..d06d5d5ebd6abc --- /dev/null +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common']); + + describe('Search session sharing', function () { + this.tags('ciGroup3'); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + loadTestFile(require.resolve('./lens')); + }); +} diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts new file mode 100644 index 00000000000000..b6dfc29bb1c6bc --- /dev/null +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['header', 'common', 'dashboard', 'timePicker', 'lens']); + + // Dashboard shares a search session with lens when navigating to and from by value lens to hit search cache + // https://github.com/elastic/kibana/issues/99310 + describe('Search session sharing with lens', () => { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/lens/basic'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/lens/basic'); + }); + + // NOTE: This test doesn't check if the cache was actually hit, but just checks if the same search session id is used + // so it doesn't give the 100% confidence that cache-hit improvement works https://github.com/elastic/kibana/issues/99310 + // but if it fails, we for sure know it doesn't work + it("should share search session with by value lens and don't share with by reference", async () => { + // Add a by ref lens panel to a new dashboard + const lensTitle = 'Artistpreviouslyknownaslens'; + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames(lensTitle); + await find.clickByButtonText(lensTitle); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + await PageObjects.dashboard.waitForRenderComplete(); + + // Navigating to lens and back should create a new session + const byRefSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.saveAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + const newByRefSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); + + expect(byRefSessionId).not.to.eql(newByRefSessionId); + + // Convert to by-value + const byRefPanel = await testSubjects.find('embeddablePanelHeading-' + lensTitle); + await dashboardPanelActions.unlinkFromLibary(byRefPanel); + await PageObjects.dashboard.waitForRenderComplete(); + const byValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); + + // Navigating to lens and back should keep the session + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.saveAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + const newByValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); + expect(byValueSessionId).to.eql(newByValueSessionId); + }); + }); +}