diff --git a/.bazelrc b/.bazelrc index 741067e4ff18e..5fa6ef245fcea 100644 --- a/.bazelrc +++ b/.bazelrc @@ -2,8 +2,17 @@ # Import shared settings first so we can override below import %workspace%/.bazelrc.common +## Disabled for now # Remote cache settings for local env # build --remote_cache=https://storage.googleapis.com/kibana-bazel-cache # build --incompatible_remote_results_ignore_disk=true # build --remote_accept_cached=true # build --remote_upload_local_results=false + +# BuildBuddy +## Metadata settings +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" +# Enable this in case you want to share your build info +# build --build_metadata=VISIBILITY=PUBLIC +build --build_metadata=TEST_GROUPS=//packages + diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 025836a90204c..a89ff166bf32e 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,9 +121,15 @@ pipeline { } def notifyStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) +} + +def notify(Map args = [:]) { + retryWithSleep(retries: 2, seconds: 5, backoff: true) { + githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) + } } diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bbdf5484faf65..ee220537de340 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -26,7 +26,6 @@ source src/dev/ci_setup/setup.sh; # download es snapshots node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; # download reporting browsers (cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3884f975c813d..34b449346ddf7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation @@ -149,7 +150,14 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations +/.bazelignore @elastic/kibana-operations +/.bazeliskversion @elastic/kibana-operations +/.bazelrc @elastic/kibana-operations +/.bazelrc.common @elastic/kibana-operations +/.bazelversion @elastic/kibana-operations +/WORKSPACE.bazel @elastic/kibana-operations #CC# /packages/kbn-expect/ @elastic/kibana-operations # Quality Assurance @@ -238,7 +246,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services @@ -306,25 +313,22 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -# Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -#CC# /x-pack/legacy/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/security_solution/ @elastic/siem - # Security Solution -/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team -#CC# /x-pack/plugins/security_solution/ @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/security-solution +/x-pack/test/security_solution_endpoint/ @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/detection_engine_api_integration @elastic/security-solution +/x-pack/test/lists_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution + +# Security Solution sub teams +/x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/plugins/lists @elastic/security-detections-response # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -356,3 +360,4 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Reporting #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000000..f64b9e95fbaab --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,46 @@ +on: + pull_request_target: + branches: + - master + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + runs-on: ubuntu-latest + + steps: + - name: 'Get backport config' + run: | + curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install backport CLI + run: npm install -g backport@5.6.4 + + - name: Backport PR + run: | + git config --global user.name "kibanamachine" + git config --global user.email "42973632+kibanamachine@users.noreply.github.com" + backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_OWNER: ${{ github.event.pull_request.user.login }} + + - name: Report backport status + run: | + COMMENT="Backport result + \`\`\` + $(cat output.log) + \`\`\`" + + GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc index 9c54ef9c8a916..3e49686fb67f0 100644 --- a/docs/developer/contributing/development-ci-metrics.asciidoc +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -44,15 +44,9 @@ All metrics are collected from the `tar.gz` archive produced for the linux platf [[ci-metric-distributable-file-count]] `distributable file count` :: The number of files included in the default distributable. -[[ci-metric-oss-distributable-file-count]] `oss distributable file count` :: -The number of files included in the OSS distributable. - [[ci-metric-distributable-size]] `distributable size` :: The size, in bytes, of the default distributable. _(not reported on PRs)_ -[[ci-metric-oss-distributable-size]] `oss distributable size` :: -The size, in bytes, of the OSS distributable. _(not reported on PRs)_ - [[ci-metric-types-saved-object-field-counts]] ==== Saved Object field counts diff --git a/docs/developer/getting-started/development-plugin-resources.asciidoc b/docs/developer/getting-started/development-plugin-resources.asciidoc index 863a67f3c42f0..9aefeabb32a55 100644 --- a/docs/developer/getting-started/development-plugin-resources.asciidoc +++ b/docs/developer/getting-started/development-plugin-resources.asciidoc @@ -14,8 +14,8 @@ You can use the <> to get a basic structure for a ne {kib} repo should be developed inside the `plugins` folder. If you are building a new plugin to check in to the {kib} repo, you will choose between a few locations: - - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for commercially licensed plugins - - {kib-repo}tree/{branch}/src/plugins[src/plugins] for open source licensed plugins + - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for plugins related to subscription features + - {kib-repo}tree/{branch}/src/plugins[src/plugins] for plugins related to free features - {kib-repo}tree/{branch}/examples[examples] for developer example plugins (these will not be included in the distributables) [discrete] diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a7..92a624649d3c5 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -71,22 +71,20 @@ export function plugin(initializerContext: PluginInitializerContext) { *plugins/my_plugin/(public|server)/plugin.ts* [source,typescript] ---- -import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; import type { MyPluginConfig } from './config'; export class MyPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: MyPluginConfig; private readonly log: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: Record) { - const isEnabled = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup, deps: Record) { + const { someConfigValue } = this.config; } } ---- @@ -96,7 +94,7 @@ Additionally, some plugins need to access the runtime env configuration. [source,typescript] ---- export class MyPlugin implements Plugin { - public async setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup, deps: Record) { const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md new file mode 100644 index 0000000000000..cf315e1fd337e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-public.asyncplugin.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md new file mode 100644 index 0000000000000..54507b44cdd72 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [setup](./kibana-plugin-core-public.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md new file mode 100644 index 0000000000000..f16d3c46bf849 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [start](./kibana-plugin-core-public.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md new file mode 100644 index 0000000000000..f809f75783c26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [stop](./kibana-plugin-core-public.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index efd499823ffad..e307b5c9971b0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -39,6 +39,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md index 7fa05588a3301..232851cd342ce 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md index 0d3c19a8217a6..ec5ed211a9d2b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md index 1fcc2999dfd2e..b7c3e11e492bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `public` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md new file mode 100644 index 0000000000000..1ad1d87220b74 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-server.asyncplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md new file mode 100644 index 0000000000000..1d033b7b88b05 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [setup](./kibana-plugin-core-server.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md new file mode 100644 index 0000000000000..3cce90f01603b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [start](./kibana-plugin-core-server.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md new file mode 100644 index 0000000000000..9272fc2c4eba0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [stop](./kibana-plugin-core-server.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 82f4a285409c9..5fe5eda7a8172 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -49,6 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md index b4e6623098736..a8b0aae28d251 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md index 03e889a018b6f..851f84474fe11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index 839eabff29a18..fe55e131065dd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md new file mode 100644 index 0000000000000..2b897db7bba4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.createIndexAliasNotFoundError() method + +Signature: + +```typescript +static createIndexAliasNotFoundError(alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md new file mode 100644 index 0000000000000..c7e10fc42ead1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError() method + +Signature: + +```typescript +static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md new file mode 100644 index 0000000000000..4b4ede2f77a7e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isGeneralError](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) + +## SavedObjectsErrorHelpers.isGeneralError() method + +Signature: + +```typescript +static isGeneralError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 9b69012ed5f12..2dc78f2df3a83 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | @@ -27,6 +28,7 @@ export declare class SavedObjectsErrorHelpers | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | +| [decorateIndexAliasNotFoundError(error, alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) | static | | | [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | static | | | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | | [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | static | | @@ -35,6 +37,7 @@ export declare class SavedObjectsErrorHelpers | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | +| [isGeneralError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) | static | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | | [isNotAuthorizedError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md) | static | | | [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | static | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index 3ba3c862bf16a..608d738676bcf 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -22,4 +22,5 @@ export declare class IndexPatternsFetcher | --- | --- | --- | | [getFieldsForTimePattern(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsfortimepattern.md) | | Get a list of field objects for a time pattern | | [getFieldsForWildcard(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md) | | Get a list of field objects for an index pattern that may contain wildcards | +| [validatePatternListActive(patternList)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) | | Returns an index pattern list of only those index pattern strings in the given list that return indices | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md new file mode 100644 index 0000000000000..8944c41204323 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) > [validatePatternListActive](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) + +## IndexPatternsFetcher.validatePatternListActive() method + +Returns an index pattern list of only those index pattern strings in the given list that return indices + +Signature: + +```typescript +validatePatternListActive(patternList: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| patternList | string[] | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 5c1a6a0393c2e..034f9c70e389f 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -4,11 +4,20 @@ ## EmbeddableStateTransfer.clearEditorState() method +Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id + Signature: ```typescript -clearEditorState(): void; +clearEditorState(appId: string): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | + Returns: `void` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md index 1434de2c9870e..cd261bff5905b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEditorState() method -Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage +Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id Signature: ```typescript -getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; +getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md index 9ead71f0bb22c..47873c8e91e41 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method -Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage +Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId Signature: ```typescript -getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; +getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch EmbeddablePackageState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md index 76b6708b93bd1..13c6c8c0325f1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md @@ -29,9 +29,9 @@ export declare class EmbeddableStateTransfer | Method | Modifiers | Description | | --- | --- | --- | -| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | | -| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage | -| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage | +| [clearEditorState(appId)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEditorState(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEmbeddablePackage(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId | | [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) | | [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) | diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 83e7edc5a016a..d7a9373a6e2a9 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -14,7 +14,7 @@ Name:: The name of the connector. The name is used to identify a connector Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -92,6 +92,8 @@ systems, refer to: * <> * <> +For other email servers, you can check the list of well-known services that Nodemailer supports in the JSON file https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. The properties of the objects in those files — `host`, `port`, and `secure` — correspond to the same email action configuration properties. A missing `secure` property in the "well-known/services.json" file is considered `false`. Typically, `port: 465` uses `secure: true`, and `port: 25` and `port: 587` use `secure: false`. + [float] [[gmail]] ===== Sending email from Gmail @@ -109,7 +111,6 @@ https://mail.google.com[Gmail] SMTP service: user: password: -------------------------------------------------- -// CONSOLE If you get an authentication error that indicates that you need to continue the sign-in process from a web browser when the action attempts to send email, you need @@ -131,9 +132,9 @@ https://www.outlook.com/[Outlook.com] SMTP service: [source,text] -------------------------------------------------- config: - host: smtp-mail.outlook.com - port: 465 - secure: true + host: smtp.office365.com + port: 587 + secure: false secrets: user: password: @@ -163,7 +164,7 @@ secrets: user: password: -------------------------------------------------- -<1> `smtp.host` varies depending on the region +<1> `config.host` varies depending on the region NOTE: You must use your Amazon SES SMTP credentials to send email through Amazon SES. For more information, see diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc index f79885e3bc716..d9073ecca1145 100644 --- a/docs/user/alerting/geo-alert-types.asciidoc +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -1,19 +1,16 @@ [role="xpack"] -[[geo-alert-types]] -== Geo alert types +[[geo-alerting]] +== Geo alerting -Two additional stack alerts are available: -<> and <>. +Alerting now includes one additional stack alert: <>. As with other stack alerts, you need `all` access to the *Stack Alerts* feature -to be able to create and edit either of the geo alerts. +to be able to create and edit a geo alert. See <> for more information on configuring roles that provide access to this feature. [float] -=== Geo alert requirements - -To create either a *Tracking threshold* or a *Tracking containment* alert, the -following requirements must be present: +=== Geo alerting requirements +To create a *Tracking containment* alert, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -33,62 +30,12 @@ than the current time minus the amount of the interval. If data older than [float] === Creating a geo alert -Both *threshold* and *containment* alerts can be created by clicking the *Create* -button in the <>. +Click the *Create* button in the <>. Complete the <>. -Select <> to generate an alert when an entity crosses a boundary, and you desire the -ability to highlight lines of crossing on a custom map. -Select -<> if an entity should send out constant alerts -while contained within a boundary (this feature is optional) or if the alert is generally -just more focused around activity when an entity exists within a shape. [role="screenshot"] image::images/alert-types-tracking-select.png[Choosing a tracking alert type] -[NOTE] -================================================== -With recent advances in the alerting framework, most of the features -available in Tracking threshold alerts can be replicated with just -a little more work in Tracking containment alerts. The capabilities of Tracking -threshold alerts may be deprecated or folded into Tracking containment alerts -in the future. -================================================== - -[float] -[[alert-type-tracking-threshold]] -=== Tracking threshold -The Tracking threshold alert type runs an {es} query over indices, comparing the latest -entity locations with their previous locations. In the event that an entity has crossed a -boundary from the selected boundary index, an alert may be generated. - -[float] -==== Defining the conditions -Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the -condition to detect, as well as 2 Kuery bars used to provide additional filtering -context for each of the indices. - -[role="screenshot"] -image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] - - -Delayed evaluation offset:: If a data source lags or is intermittent, you may supply -an optional value to evaluate alert conditions following a fixed delay. For instance, if data -is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation -offset* of `10 minutes` would ensure that alertable instances are still captured. -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. -By:: This clause specifies the field to use in the previously provided -*index or index pattern* for tracking Entities. An entity is a `keyword` -or `number` field that consistently identifies the entity to be tracked. -When entity:: This clause specifies which crossing option to track. The values -*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions -should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit -from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances -or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* -identifying boundaries, and an optional *Human-readable boundary name* for better alerting -messages. - [float] [[alert-type-tracking-containment]] === Tracking containment diff --git a/docs/user/alerting/images/alert-types-tracking-select.png b/docs/user/alerting/images/alert-types-tracking-select.png index 445a5202ffd0c..44fcf1a2600b8 100644 Binary files a/docs/user/alerting/images/alert-types-tracking-select.png and b/docs/user/alerting/images/alert-types-tracking-select.png differ diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 8b3eddc008500..3c86c37f1fd30 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -133,15 +133,17 @@ image::dashboard/images/dashboard-filters.png[Labeled interface with semi-struct Semi-structured search:: Combine free text search with field-based search using the <>. Type a search term to match across all fields, or begin typing a field name to - get prompted with field names and operators you can use to build a structured query. - + + get prompted with field names and operators you can use to build a structured query. For example, in the sample web logs data, this query displays data only for the US: - . Enter `g`, and then select *geo.source*. - . Select *equals some value* and *US*, and then click *Update*. + . Enter `g`, then select *geo.source*. + . Select *equals some value* and *US*, then click *Update*. . For a more complex search, try: - `geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat"` +[source,text] +------------------- +geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" +------------------- Time filter:: Dashboards have a global time filter that restricts the data that displays, but individual panels can @@ -152,21 +154,18 @@ Time filter:: . Open the panel menu, then select *More > Customize time range*. . On the *Customize panel time range* window, specify the new time range, then click *Add to panel*. - + [role="screenshot"] image:images/time_range_per_panel.gif[Time range per dashboard panel] Additional filters with AND:: - You can add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. - + Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. When you use more than one index pattern on a dashboard, the filter editor allows you to filter only one dashboard. - To dynamically add filters, click a series on a dashboard. For example, to filter the dashboard to display only ios data: - . Click *Add filter*. . Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. . *Save* the filter. - . To remove the filter, click *x* next to the filter. + . To remove the filter, click *x*. [float] [[clone-panels]] diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 1f07b0b57d8c7..ebe095e0881b3 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -15,6 +15,7 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> * <> +* <> [float] [[reporting-diagnostics]] @@ -156,3 +157,9 @@ requests to render. If the {kib} instance doesn't have enough memory to run the report, the report fails with an error such as `Error: Page crashed!` In this case, try increasing the memory for the {kib} instance to 2GB. + +[float] +[[reporting-troubleshooting-arm-systems]] +=== ARM systems + +Chromium is not compatible with ARM RHEL/CentOS. diff --git a/package.json b/package.json index fc5cd02a03253..7144745f2ae35 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", "idx": "^2.5.6", - "immer": "^1.5.0", + "immer": "^8.0.1", "inline-style": "^2.0.0", "intl": "^1.2.5", "intl-format-cache": "^2.1.0", @@ -558,6 +558,7 @@ "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.3", "@types/webpack-merge": "^4.1.5", + "@types/webpack-sources": "^0.1.4", "@types/write-pkg": "^3.1.0", "@types/xml-crypto": "^1.4.1", "@types/xml2js": "^0.4.5", @@ -636,7 +637,7 @@ "eslint-plugin-ban": "^1.4.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.19.1", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.0.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-mocha": "^6.2.2", @@ -714,7 +715,6 @@ "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", @@ -833,6 +833,7 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", + "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", @@ -843,6 +844,7 @@ "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", + "webpack-sources": "^1.4.1", "write-pkg": "^4.0.0", "xml-crypto": "^2.0.0", "xmlbuilder": "13.0.2", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index cb5175142c160..93826cf3add80 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -109,7 +109,7 @@ export class CiStatsReporter { }, }); - return; + return true; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts index 165239cbebb89..d99217c38b410 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -7,3 +7,4 @@ */ export * from './ci_stats_reporter'; +export * from './ship_ci_stats_cli'; diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts new file mode 100644 index 0000000000000..1ee78518bb801 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { CiStatsReporter } from './ci_stats_reporter'; +import { run, createFlagError, createFailError } from '../run'; + +export function shipCiStatsCli() { + run( + async ({ log, flags }) => { + let metricPaths = flags.metrics; + if (typeof metricPaths === 'string') { + metricPaths = [metricPaths]; + } else if (!Array.isArray(metricPaths) || !metricPaths.every((p) => typeof p === 'string')) { + throw createFlagError('expected --metrics to be a string'); + } + + const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('unable to initilize the CI Stats reporter'); + } + + for (const path of metricPaths) { + // resolve path from CLI relative to CWD + const abs = Path.resolve(path); + const json = Fs.readFileSync(abs, 'utf8'); + if (await reporter.metrics(JSON.parse(json))) { + log.success('shipped metrics from', path); + } else { + throw createFailError('failed to ship metrics'); + } + } + }, + { + description: 'ship ci-stats which have been written to files', + usage: `node scripts/ship_ci_stats`, + log: { + defaultLevel: 'debug', + }, + flags: { + string: ['metrics'], + help: ` + --metrics [path] A path to a JSON file that includes metrics which should be sent. Multiple instances supported + `, + }, + } + ); +} diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index d3494512d055a..f86865ffa6670 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -25,5 +25,6 @@ export async function emptyKibanaIndexAction({ await cleanKibanaIndices({ client, stats, log, kibanaPluginIds }); await migrateKibanaIndex({ client, kbnClient }); - return stats; + stats.createdIndex('.kibana'); + return stats.toJSON(); } diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 70dc5370c5a26..b00b9fb8b3f25 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -155,7 +155,7 @@ export class EsArchiver { * @return Promise */ async emptyKibanaIndex() { - await emptyKibanaIndexAction({ + return await emptyKibanaIndexAction({ client: this.client, log: this.log, kbnClient: this.kbnClient, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 6d48c0b2bbaea..64e5626c94c8b 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -82,7 +82,9 @@ export async function migrateKibanaIndex({ */ async function fetchKibanaIndices(client: Client) { const resp = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + const isKibanaIndex = (index: string) => + /^\.kibana(:?_\d*)?$/.test(index) || + /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index); if (!Array.isArray(resp.body)) { throw new Error(`expected response to be an array ${inspect(resp.body)}`); @@ -115,7 +117,7 @@ export async function cleanKibanaIndices({ while (true) { const resp = await client.deleteByQuery( { - index: `.kibana`, + index: `.kibana,.kibana_task_manager`, body: { query: { bool: { @@ -129,7 +131,7 @@ export async function cleanKibanaIndices({ }, }, { - ignore: [409], + ignore: [404, 409], } ); diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 3021982b8ed6a..8fb906aa4603e 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -12,11 +12,10 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; -import { run, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; -import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; @@ -120,17 +119,7 @@ run( return; } - let update$ = runOptimizer(config); - - if (reportStats) { - const reporter = CiStatsReporter.fromEnv(log); - - if (!reporter.isEnabled()) { - log.warning('Unable to initialize CiStatsReporter from env'); - } - - update$ = update$.pipe(reportOptimizerStats(reporter, config, log)); - } + const update$ = runOptimizer(config); await lastValueFrom(update$.pipe(logOptimizerState(log, config))); @@ -153,7 +142,6 @@ run( 'cache', 'profile', 'inspect-workers', - 'report-stats', 'validate-limits', 'update-limits', ], @@ -179,7 +167,6 @@ run( --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers - --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb `, diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index b6d25f69e58b4..ff9aa6fd90628 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -42,6 +42,7 @@ it('creates cache keys', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -79,6 +80,7 @@ it('parses bundles from JSON specs', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index cb6096759739b..64b44de0dd1b3 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -36,6 +36,8 @@ export interface BundleSpec { readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; + /** Maximum allowed page load asset size for the bundles page load asset */ + readonly pageLoadAssetSizeLimit?: number; } export class Bundle { @@ -63,6 +65,8 @@ export class Bundle { * Every bundle mentioned in the `requiredBundles` must be built together. */ public readonly manifestPath: BundleSpec['manifestPath']; + /** Maximum allowed page load asset size for the bundles page load asset */ + public readonly pageLoadAssetSizeLimit: BundleSpec['pageLoadAssetSizeLimit']; public readonly cache: BundleCache; @@ -75,8 +79,9 @@ export class Bundle { this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; this.banner = spec.banner; + this.pageLoadAssetSizeLimit = spec.pageLoadAssetSizeLimit; - this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); + this.cache = new BundleCache(this.outputDir); } /** @@ -107,6 +112,7 @@ export class Bundle { outputDir: this.outputDir, manifestPath: this.manifestPath, banner: this.banner, + pageLoadAssetSizeLimit: this.pageLoadAssetSizeLimit, }; } @@ -222,6 +228,13 @@ export function parseBundles(json: string) { } } + const { pageLoadAssetSizeLimit } = spec; + if (pageLoadAssetSizeLimit !== undefined) { + if (!(typeof pageLoadAssetSizeLimit === 'number')) { + throw new Error('`bundles[]` must have a numeric `pageLoadAssetSizeLimit` property'); + } + } + return new Bundle({ type, id, @@ -231,6 +244,7 @@ export function parseBundles(json: string) { outputDir, banner, manifestPath, + pageLoadAssetSizeLimit, }); } ); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.test.ts b/packages/kbn-optimizer/src/common/bundle_cache.test.ts index 82a8c0debb83c..e903a687908b9 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.test.ts @@ -25,12 +25,12 @@ beforeEach(() => { }); it(`doesn't complain if files are not on disk`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.get()).toEqual({}); }); it(`updates files on disk when calling set()`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); expect(mockReadFileSync).not.toHaveBeenCalled(); expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` @@ -46,7 +46,7 @@ it(`updates files on disk when calling set()`, () => { expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "{ \\"cacheKey\\": \\"abc\\", \\"files\\": [ @@ -61,7 +61,7 @@ it(`updates files on disk when calling set()`, () => { }); it(`serves updated state from memory`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); jest.clearAllMocks(); @@ -72,7 +72,7 @@ it(`serves updated state from memory`, () => { }); it('reads state from disk on get() after refresh()', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); cache.refresh(); jest.clearAllMocks(); @@ -83,7 +83,7 @@ it('reads state from disk on get() after refresh()', () => { expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "utf8", ], ] @@ -91,7 +91,7 @@ it('reads state from disk on get() after refresh()', () => { }); it('provides accessors to specific state properties', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.getModuleCount()).toBe(undefined); expect(cache.getReferencedFiles()).toEqual(undefined); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 39b52095c819a..7c0770caa2623 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -9,6 +9,9 @@ import Fs from 'fs'; import Path from 'path'; +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; + export interface State { optimizerCacheKey?: unknown; cacheKey?: unknown; @@ -20,13 +23,17 @@ export interface State { const DEFAULT_STATE: State = {}; const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); +const CACHE_FILENAME = '.kbn-optimizer-cache'; /** * Helper to read and update metadata for bundles. */ export class BundleCache { private state: State | undefined = undefined; - constructor(private readonly path: string | false) {} + private readonly path: string | false; + constructor(outputDir: string | false) { + this.path = outputDir === false ? false : Path.resolve(outputDir, CACHE_FILENAME); + } refresh() { this.state = undefined; @@ -63,6 +70,7 @@ export class BundleCache { set(updated: State) { this.state = updated; + if (this.path) { const directory = Path.dirname(this.path); Fs.mkdirSync(directory, { recursive: true }); @@ -107,4 +115,16 @@ export class BundleCache { } } } + + public writeWebpackAsset(compilation: webpack.compilation.Compilation) { + if (!this.path) { + return; + } + + const source = new RawSource(JSON.stringify(this.state, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset(CACHE_FILENAME, source); + } } diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index a74679bfff536..551d2ffacfcfb 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -9,6 +9,5 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './report_optimizer_stats'; export * from './node'; export * from './limits'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1ed1b92f9c2d9..9e9e8960da21b 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -13,6 +13,7 @@ OptimizerConfig { "id": "bar", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -29,6 +30,7 @@ OptimizerConfig { "id": "foo", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -47,6 +49,7 @@ OptimizerConfig { "id": "baz", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -57,7 +60,6 @@ OptimizerConfig { "cache": true, "dist": false, "inspectWorkers": false, - "limits": "", "maxWorkerCount": 1, "plugins": Array [ Object { @@ -109,3 +111,34 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { dist: false, }); - expect(config.limits).toEqual(readLimits()); - (config as any).limits = ''; - expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await allValuesFrom( @@ -235,6 +226,10 @@ it('prepares assets for distribution', async () => { await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/metrics.json'), 'utf8') + ).toMatchSnapshot('metrics.json'); + expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/foo.chunk.1.js', diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index fcfd36664c1f4..292314a4608e4 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -7,12 +7,13 @@ */ import Fs from 'fs'; +import Path from 'path'; import dedent from 'dedent'; import Yaml from 'js-yaml'; -import { createFailError, ToolingLog } from '@kbn/dev-utils'; +import { createFailError, ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig, getMetrics, Limits } from './optimizer'; +import { OptimizerConfig, Limits } from './optimizer'; const LIMITS_PATH = require.resolve('../limits.yml'); const DEFAULT_BUDGET = 15000; @@ -33,7 +34,7 @@ export function readLimits(): Limits { } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { - const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); + const limitBundleIds = Object.keys(readLimits().pageLoadAssetSize || {}); const configBundleIds = config.bundles.map((b) => b.id); const missingBundleIds = diff(configBundleIds, limitBundleIds); @@ -75,15 +76,21 @@ interface UpdateBundleLimitsOptions { } export function updateBundleLimits({ log, config, dropMissing }: UpdateBundleLimitsOptions) { - const metrics = getMetrics(log, config); + const limits = readLimits(); + const metrics: CiStatsMetrics = config.bundles + .map((bundle) => + JSON.parse(Fs.readFileSync(Path.resolve(bundle.outputDir, 'metrics.json'), 'utf-8')) + ) + .flat() + .sort((a, b) => a.id.localeCompare(b.id)); const pageLoadAssetSize: NonNullable = dropMissing ? {} - : config.limits.pageLoadAssetSize ?? {}; + : limits.pageLoadAssetSize ?? {}; - for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { + for (const metric of metrics) { if (metric.group === 'page load bundle size') { - const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; + const existingLimit = limits.pageLoadAssetSize?.[metric.id]; pageLoadAssetSize[metric.id] = existingLimit != null && existingLimit >= metric.value ? existingLimit diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts deleted file mode 100644 index e7059c4d6799c..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Fs from 'fs'; -import Path from 'path'; - -import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; - -const flatten = (arr: Array): T[] => - arr.reduce((acc: T[], item) => acc.concat(item), []); - -interface Entry { - relPath: string; - stats: Fs.Stats; -} - -const IGNORED_EXTNAME = ['.map', '.br', '.gz']; - -const getFiles = (dir: string, parent?: string) => - flatten( - Fs.readdirSync(dir).map((name): Entry | Entry[] => { - const absPath = Path.join(dir, name); - const relPath = parent ? Path.join(parent, name) : name; - const stats = Fs.statSync(absPath); - - if (stats.isDirectory()) { - return getFiles(absPath, relPath); - } - - return { - relPath, - stats, - }; - }) - ).filter((file) => { - const filename = Path.basename(file.relPath); - if (filename.startsWith('.')) { - return false; - } - - const ext = Path.extname(filename); - if (IGNORED_EXTNAME.includes(ext)) { - return false; - } - - return true; - }); - -export function getMetrics(log: ToolingLog, config: OptimizerConfig) { - return flatten( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - const outputFiles = getFiles(bundle.outputDir); - const entryName = `${bundle.id}.${bundle.type}.js`; - const entry = outputFiles.find((f) => f.relPath === entryName); - if (!entry) { - throw new Error( - `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` - ); - } - - const chunkPrefix = `${bundle.id}.chunk.`; - const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); - const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f)); - - if (asyncChunks.length) { - log.verbose(bundle.id, 'async chunks', asyncChunks); - } - if (miscFiles.length) { - log.verbose(bundle.id, 'misc files', asyncChunks); - } - - const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); - - const bundleMetrics: CiStatsMetrics = [ - { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }, - { - group: `page load bundle size`, - id: bundle.id, - value: entry.stats!.size, - limit: config.limits.pageLoadAssetSize?.[bundle.id], - limitConfigPath: `packages/kbn-optimizer/limits.yml`, - }, - { - group: `async chunks size`, - id: bundle.id, - value: sumSize(asyncChunks), - }, - { - group: `async chunk count`, - id: bundle.id, - value: asyncChunks.length, - }, - { - group: `miscellaneous assets size`, - id: bundle.id, - value: sumSize(miscFiles), - }, - ]; - - log.debug(bundle.id, 'metrics', bundleMetrics); - - return bundleMetrics; - }) - ); -} diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index d921d5e5cca31..e4cdddbf56dcb 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,7 +48,12 @@ it('returns a bundle for core and each plugin', () => { }, ], '/repo', - '/output' + '/output', + { + pageLoadAssetSize: { + box: 123, + }, + } ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ @@ -58,6 +63,7 @@ it('returns a bundle for core and each plugin', () => { "id": "foo", "manifestPath": /plugins/foo/kibana.json, "outputDir": /plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -70,6 +76,7 @@ it('returns a bundle for core and each plugin', () => { "id": "baz", "manifestPath": /plugins/baz/kibana.json, "outputDir": /plugins/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -84,6 +91,7 @@ it('returns a bundle for core and each plugin', () => { "id": "box", "manifestPath": /x-pack/plugins/box/kibana.json, "outputDir": /x-pack/plugins/box/target/public, + "pageLoadAssetSizeLimit": 123, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 76a0d51edac82..8134707561bc0 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -9,13 +9,15 @@ import Path from 'path'; import { Bundle } from '../common'; +import { Limits } from './optimizer_config'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles( plugins: KibanaPlatformPlugin[], repoRoot: string, - outputRoot: string + outputRoot: string, + limits: Limits ) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; @@ -39,6 +41,7 @@ export function getPluginBundles( ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. \n` + ` * Licensed under the Elastic License 2.0; you may not use this file except in compliance with the Elastic License 2.0. */\n` : undefined, + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.[p.id], }) ); } diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts index ced61463d5edd..28d206488b0a4 100644 --- a/packages/kbn-optimizer/src/optimizer/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -14,4 +14,3 @@ export * from './watch_bundles_for_changes'; export * from './run_workers'; export * from './bundle_cache'; export * from './handle_optimizer_completion'; -export * from './get_output_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 5677719628b6a..c60d6719cdea7 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -435,7 +435,6 @@ describe('OptimizerConfig::create()', () => { "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), - "limits": Symbol(limits), "maxWorkerCount": Symbol(parsed max worker count), "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), @@ -457,7 +456,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 21, + 22, ], "results": Array [ Object { @@ -480,7 +479,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 24, + 25, ], "results": Array [ Object { @@ -498,13 +497,14 @@ describe('OptimizerConfig::create()', () => { Symbol(new platform plugins), Symbol(parsed repo root), Symbol(parsed output root), + Symbol(limits), ], ], "instances": Array [ [Window], ], "invocationCallOrder": Array [ - 22, + 23, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b93d7a753c9ac..ed521d32a0a29 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -211,6 +211,7 @@ export class OptimizerConfig { } static create(inputOptions: Options) { + const limits = readLimits(); const options = OptimizerConfig.parseOptions(inputOptions); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); const bundles = [ @@ -223,10 +224,11 @@ export class OptimizerConfig { sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.core, }), ] : []), - ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot, limits), ]; return new OptimizerConfig( @@ -239,8 +241,7 @@ export class OptimizerConfig { options.maxWorkerCount, options.dist, options.profileWebpack, - options.themeTags, - readLimits() + options.themeTags ); } @@ -254,8 +255,7 @@ export class OptimizerConfig { public readonly maxWorkerCount: number, public readonly dist: boolean, public readonly profileWebpack: boolean, - public readonly themeTags: ThemeTags, - public readonly limits: Limits + public readonly themeTags: ThemeTags ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts deleted file mode 100644 index eeed2fb1b156c..0000000000000 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; - -import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerConfig, getMetrics } from './optimizer'; -import { pipeClosure } from './common'; - -export function reportOptimizerStats( - reporter: CiStatsReporter, - config: OptimizerConfig, - log: ToolingLog -) { - return pipeClosure((update$: OptimizerUpdate$) => - update$.pipe( - materialize(), - mergeMap(async (n) => { - if (n.kind === 'C') { - const metrics = getMetrics(log, config); - - await reporter.metrics(metrics); - - for (const metric of metrics) { - if (metric.limit != null && metric.value > metric.limit) { - const value = metric.value.toLocaleString(); - const limit = metric.limit.toLocaleString(); - log.warning( - `Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]` - ); - } - } - } - - return n; - }), - dematerialize() - ) - ); -} diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts new file mode 100644 index 0000000000000..909a97a3e11c7 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; +import { CiStatsMetrics } from '@kbn/dev-utils'; + +import { Bundle } from '../common'; + +interface Asset { + name: string; + size: number; +} + +const IGNORED_EXTNAME = ['.map', '.br', '.gz']; + +export class BundleMetricsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle } = this; + + compiler.hooks.emit.tap('BundleMetricsPlugin', (compilation) => { + const assets = Object.entries(compilation.assets) + .map( + ([name, source]: [string, any]): Asset => ({ + name, + size: source.size(), + }) + ) + .filter((asset) => { + const filename = Path.basename(asset.name); + if (filename.startsWith('.')) { + return false; + } + + const ext = Path.extname(filename); + if (IGNORED_EXTNAME.includes(ext)) { + return false; + } + + return true; + }); + + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = assets.find((a) => a.name === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = assets.filter((a) => a.name.startsWith(chunkPrefix)); + const miscFiles = assets.filter((a) => a !== entry && !asyncChunks.includes(a)); + + const sumSize = (files: Asset[]) => files.reduce((acc: number, a) => acc + a.size, 0); + + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); + } + + const bundleMetrics: CiStatsMetrics = [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: moduleCount, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.size, + limit: bundle.pageLoadAssetSizeLimit, + limitConfigPath: `packages/kbn-optimizer/limits.yml`, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + + const metricsSource = new RawSource(JSON.stringify(bundleMetrics, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset('metrics.json', metricsSource); + }); + } +} diff --git a/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts new file mode 100644 index 0000000000000..c964219e1fed6 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +import webpack from 'webpack'; + +import { Bundle } from '../common'; + +export class EmitStatsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + compiler.hooks.done.tap( + { + name: 'EmitStatsPlugin', + // run at the very end, ensure that it's after clean-webpack-plugin + stage: 10, + }, + (stats) => { + Fs.writeFileSync( + Path.resolve(this.bundle.outputDir, 'stats.json'), + JSON.stringify(stats.toJson()) + ); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts new file mode 100644 index 0000000000000..6d296b9be089c --- /dev/null +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import webpack from 'webpack'; + +import Path from 'path'; +import { inspect } from 'util'; + +import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common'; +import { BundleRefModule } from './bundle_ref_module'; +import { + isExternalModule, + isNormalModule, + isIgnoredModule, + isConcatenatedModule, + getModulePath, +} from './webpack_helpers'; + +/** + * sass-loader creates about a 40% overhead on the overall optimizer runtime, and + * so this constant is used to indicate to assignBundlesToWorkers() that there is + * extra work done in a bundle that has a lot of scss imports. The value is + * arbitrary and just intended to weigh the bundles so that they are distributed + * across mulitple workers on machines with lots of cores. + */ +const EXTRA_SCSS_WORK_UNITS = 100; + +export class PopulateBundleCachePlugin { + constructor(private readonly workerConfig: WorkerConfig, private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle, workerConfig } = this; + + compiler.hooks.emit.tap( + { + name: 'PopulateBundleCachePlugin', + before: ['BundleMetricsPlugin'], + }, + (compilation) => { + const bundleRefExportIds: string[] = []; + const referencedFiles = new Set(); + let moduleCount = 0; + let workUnits = compilation.fileDependencies.size; + + if (bundle.manifestPath) { + referencedFiles.add(bundle.manifestPath); + } + + for (const module of compilation.modules) { + if (isNormalModule(module)) { + moduleCount += 1; + const path = getModulePath(module); + const parsedPath = parseFilePath(path); + + if (!parsedPath.dirs.includes('node_modules')) { + referencedFiles.add(path); + + if (path.endsWith('.scss')) { + workUnits += EXTRA_SCSS_WORK_UNITS; + + for (const depPath of module.buildInfo.fileDependencies) { + referencedFiles.add(depPath); + } + } + + continue; + } + + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + continue; + } + + if (module instanceof BundleRefModule) { + bundleRefExportIds.push(module.ref.exportId); + continue; + } + + if (isConcatenatedModule(module)) { + moduleCount += module.modules.length; + continue; + } + + if (isExternalModule(module) || isIgnoredModule(module)) { + continue; + } + + throw new Error(`Unexpected module type: ${inspect(module)}`); + } + + const files = Array.from(referencedFiles).sort(ascending((p) => p)); + const mtimes = new Map( + files.map((path): [string, number | undefined] => { + try { + return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; + } catch (error) { + if (error?.code === 'ENOENT') { + return [path, undefined]; + } + + throw error; + } + }) + ); + + bundle.cache.set({ + bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), + optimizerCacheKey: workerConfig.optimizerCacheKey, + cacheKey: bundle.createCacheKey(files, mtimes), + moduleCount, + workUnits, + files, + }); + + // write the cache to the compilation so that it isn't cleaned by clean-webpack-plugin + bundle.cache.writeWebpackAsset(compilation); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 61f9c243a4def..4f5bb23c3550d 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -8,46 +8,16 @@ import 'source-map-support/register'; -import Fs from 'fs'; -import Path from 'path'; -import { inspect } from 'util'; - import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { - CompilerMsgs, - CompilerMsg, - maybeMap, - Bundle, - WorkerConfig, - ascending, - parseFilePath, - BundleRefs, -} from '../common'; -import { BundleRefModule } from './bundle_ref_module'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, BundleRefs } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; -import { - isExternalModule, - isNormalModule, - isIgnoredModule, - isConcatenatedModule, - getModulePath, -} from './webpack_helpers'; const PLUGIN_NAME = '@kbn/optimizer'; -/** - * sass-loader creates about a 40% overhead on the overall optimizer runtime, and - * so this constant is used to indicate to assignBundlesToWorkers() that there is - * extra work done in a bundle that has a lot of scss imports. The value is - * arbitrary and just intended to weigh the bundles so that they are distributed - * across mulitple workers on machines with lots of cores. - */ -const EXTRA_SCSS_WORK_UNITS = 100; - /** * Create an Observable for a specific child compiler + bundle */ @@ -80,13 +50,6 @@ const observeCompiler = ( return undefined; } - if (workerConfig.profileWebpack) { - Fs.writeFileSync( - Path.resolve(bundle.outputDir, 'stats.json'), - JSON.stringify(stats.toJson()) - ); - } - if (!workerConfig.watch) { process.nextTick(() => done$.next()); } @@ -97,88 +60,11 @@ const observeCompiler = ( }); } - const bundleRefExportIds: string[] = []; - const referencedFiles = new Set(); - let moduleCount = 0; - let workUnits = stats.compilation.fileDependencies.size; - - if (bundle.manifestPath) { - referencedFiles.add(bundle.manifestPath); - } - - for (const module of stats.compilation.modules) { - if (isNormalModule(module)) { - moduleCount += 1; - const path = getModulePath(module); - const parsedPath = parseFilePath(path); - - if (!parsedPath.dirs.includes('node_modules')) { - referencedFiles.add(path); - - if (path.endsWith('.scss')) { - workUnits += EXTRA_SCSS_WORK_UNITS; - - for (const depPath of module.buildInfo.fileDependencies) { - referencedFiles.add(depPath); - } - } - - continue; - } - - const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); - const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); - referencedFiles.add( - Path.join( - parsedPath.root, - ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), - 'package.json' - ) - ); - continue; - } - - if (module instanceof BundleRefModule) { - bundleRefExportIds.push(module.ref.exportId); - continue; - } - - if (isConcatenatedModule(module)) { - moduleCount += module.modules.length; - continue; - } - - if (isExternalModule(module) || isIgnoredModule(module)) { - continue; - } - - throw new Error(`Unexpected module type: ${inspect(module)}`); + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); } - const files = Array.from(referencedFiles).sort(ascending((p) => p)); - const mtimes = new Map( - files.map((path): [string, number | undefined] => { - try { - return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; - } catch (error) { - if (error?.code === 'ENOENT') { - return [path, undefined]; - } - - throw error; - } - }) - ); - - bundle.cache.set({ - bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), - optimizerCacheKey: workerConfig.optimizerCacheKey, - cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount, - workUnits, - files, - }); - return compilerMsgs.compilerSuccess({ moduleCount, }); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 331fbde6ea0ba..c4beb959284cc 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -19,6 +19,9 @@ import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, BundleRefs, WorkerConfig } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; +import { BundleMetricsPlugin } from './bundle_metrics_plugin'; +import { EmitStatsPlugin } from './emit_stats_plugin'; +import { PopulateBundleCachePlugin } from './populate_bundle_cache_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -67,6 +70,9 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + new PopulateBundleCachePlugin(worker, bundle), + new BundleMetricsPlugin(bundle), + ...(worker.profileWebpack ? [new EmitStatsPlugin(bundle)] : []), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 559d9da35c320..9723c0107cf8e 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -74,13 +74,14 @@ it('builds a generated plugin into a viable archive', async () => { await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); - const files = await globby(['**/*'], { cwd: TMP_DIR }); + const files = await globby(['**/*'], { cwd: TMP_DIR, dot: true }); files.sort((a, b) => a.localeCompare(b)); expect(files).toMatchInlineSnapshot(` Array [ "kibana/fooTestPlugin/common/index.js", "kibana/fooTestPlugin/kibana.json", + "kibana/fooTestPlugin/node_modules/.yarn-integrity", "kibana/fooTestPlugin/package.json", "kibana/fooTestPlugin/server/index.js", "kibana/fooTestPlugin/server/plugin.js", diff --git a/packages/kbn-plugin-helpers/src/tasks/optimize.ts b/packages/kbn-plugin-helpers/src/tasks/optimize.ts index 0f0ac93086c9e..2478947e79f18 100644 --- a/packages/kbn-plugin-helpers/src/tasks/optimize.ts +++ b/packages/kbn-plugin-helpers/src/tasks/optimize.ts @@ -34,9 +34,15 @@ export async function optimize({ log, plugin, sourceDir, buildDir }: BuildContex pluginScanDirs: [], }); + const target = Path.resolve(sourceDir, 'target'); + await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); + // clean up unnecessary files + Fs.unlinkSync(Path.resolve(target, 'public/metrics.json')); + Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache')); + // move target into buildDir - await asyncRename(Path.resolve(sourceDir, 'target'), Path.resolve(buildDir, 'target')); + await asyncRename(target, Path.resolve(buildDir, 'target')); log.indent(-2); } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 4d065411f91b6..d939e7b3000fa 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48106,23 +48106,59 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion) { + try { + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], { + stdio: 'pipe' + }); + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { + stdio: 'pipe' + }); // Bazelisk was found on yarn global scope so lets remove it + + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe' + }); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + async function installBazelTools(repoRootPath) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed - _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); - const { - stdout: bazeliskPkgInstallStdout - } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { - stdio: 'pipe' - }); - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed + _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Test if bazelisk is already installed in the correct version - if (!bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || !isBazelBinAlreadyAvailable) { + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available + + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Check if we need to remove bazelisk from yarn + + await tryRemoveBazeliskFromYarnGlobal(); // Install bazelisk if not installed + + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}`); - await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion }, @@ -48132,7 +48168,7 @@ async function installBazelTools(repoRootPath) { if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent__WEBPACK_IMPORTED_MODULE_0___default.a` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } @@ -59771,10 +59807,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(745); -/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(248); +/* harmony import */ var _utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(374); +/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(251); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(248); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -59790,17 +59827,19 @@ __webpack_require__.r(__webpack_exports__); + async function buildBazelProductionProjects({ kibanaRoot, buildRoot, onlyOSS }) { - const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_7__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); + const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_8__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); const projectNames = [...projects.values()].map(project => project.name); - _utils_log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await Object(_utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); for (const project of projects.values()) { - await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["buildProject"])(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } @@ -59835,9 +59874,9 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { // the intermediate build, we fall back to using the project's already defined // `package.json`. - const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["readPackageJson"])(buildProjectPath) : project.json; - const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["createProductionPackageJson"])(packageJson); - await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["writePackageJson"])(buildProjectPath, preparedPackageJson); + const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["readPackageJson"])(buildProjectPath) : project.json; + const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["createProductionPackageJson"])(packageJson); + await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["writePackageJson"])(buildProjectPath, preparedPackageJson); } async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { @@ -59852,12 +59891,12 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { for (const pluginPath of allPluginPaths) { const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o644); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); } - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isDirectory"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o755); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isDirectory"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o755); } } } diff --git a/packages/kbn-pm/src/production/build_bazel_production_projects.ts b/packages/kbn-pm/src/production/build_bazel_production_projects.ts index cd40653a6b54c..a54d6c753d8d7 100644 --- a/packages/kbn-pm/src/production/build_bazel_production_projects.ts +++ b/packages/kbn-pm/src/production/build_bazel_production_projects.ts @@ -10,7 +10,8 @@ import copy from 'cpy'; import globby from 'globby'; import { basename, join, relative, resolve } from 'path'; -import { buildProject, getProductionProjects } from './build_non_bazel_production_projects'; +import { getProductionProjects } from './build_non_bazel_production_projects'; +import { runBazel } from '../utils/bazel/run'; import { chmod, isFile, isDirectory } from '../utils/fs'; import { log } from '../utils/log'; import { @@ -35,8 +36,10 @@ export async function buildBazelProductionProjects({ const projectNames = [...projects.values()].map((project) => project.name); log.info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await runBazel(['build', '//packages:build']); + log.info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + for (const project of projects.values()) { - await buildProject(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index dfd20f5974d67..b547c2bc141bd 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -36,6 +36,45 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion: string) { + try { + const { stdout: bazeliskPkgInstallStdout } = await spawn( + 'npm', + ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], + { + stdio: 'pipe', + } + ); + + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { + stdio: 'pipe', + }); + + // Bazelisk was found on yarn global scope so lets remove it + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await spawn('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe', + }); + + log.info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + export async function installBazelTools(repoRootPath: string) { log.debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -43,23 +82,24 @@ export async function installBazelTools(repoRootPath: string) { // Check what globals are installed log.debug(`[bazel_tools] verify if bazelisk is installed`); - const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { - stdio: 'pipe', - }); + // Test if bazelisk is already installed in the correct version + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); + + // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); + // Check if we need to remove bazelisk from yarn + await tryRemoveBazeliskFromYarnGlobal(); + // Install bazelisk if not installed - if ( - !bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || - !isBazelBinAlreadyAvailable - ) { + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); log.debug( `[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}` ); - await spawn('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await spawn('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion, }, @@ -69,7 +109,7 @@ export async function installBazelTools(repoRootPath: string) { const isBazelBinAvailableAfterInstall = await isBazelBinAvailable(); if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19..d79594c97cec7 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -12,7 +12,7 @@ export { get } from './get'; export { mapToObject } from './map_to_object'; export { merge } from './merge'; export { pick } from './pick'; -export { withTimeout } from './promise'; +export { withTimeout, isPromise } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index 61197a2a8bf70..f7c119acd0c7a 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from './promise'; +import { withTimeout, isPromise } from './promise'; const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); @@ -50,3 +50,30 @@ describe('withTimeout', () => { ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); }); + +describe('isPromise', () => { + it('returns true when arg is a Promise', () => { + expect(isPromise(Promise.resolve('foo'))).toEqual(true); + expect(isPromise(Promise.reject('foo').catch(() => undefined))).toEqual(true); + }); + + it('returns false when arg is not a Promise', () => { + expect(isPromise(12)).toEqual(false); + expect(isPromise('foo')).toEqual(false); + expect(isPromise({ hello: 'dolly' })).toEqual(false); + expect(isPromise([1, 2, 3])).toEqual(false); + }); + + it('returns false for objects with a non-function `then` property', () => { + expect(isPromise({ then: 'bar' })).toEqual(false); + }); + + it('returns false for null and undefined', () => { + expect(isPromise(null)).toEqual(false); + expect(isPromise(undefined)).toEqual(false); + }); + + it('returns true for Promise-Like objects', () => { + expect(isPromise({ then: () => 12 })).toEqual(true); + }); +}); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index ce4e50bf9b2ac..9d8f7703c026d 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -20,3 +20,7 @@ export function withTimeout({ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), ]) as Promise; } + +export function isPromise(maybePromise: T | Promise): maybePromise is Promise { + return maybePromise ? typeof (maybePromise as Promise).then === 'function' : false; +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 46471a4e9dac7..4fd28678d2653 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -169,7 +169,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('oss'), + license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), serverEnvVars: Joi.object(), diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc..02c55b6af91dc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,6 +95,8 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); + // workaround until https://github.com/elastic/kibana/issues/89828 is addressed + await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -160,3 +162,7 @@ async function silence(log, milliseconds) { ) .toPromise(); } + +async function delay(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index c04564279a971..43b6c90452b81 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,7 +22,7 @@ export function createLegacyEsTestCluster(options = {}) { const { port = esTestConfig.getPort(), password = 'changeme', - license = 'oss', + license = 'basic', log, basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/scripts/ship_ci_stats.js similarity index 72% rename from src/plugins/vis_type_vega/public/lib/vega.js rename to scripts/ship_ci_stats.js index b7c59fce6dec2..5aed9fc446240 100644 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ b/scripts/ship_ci_stats.js @@ -6,7 +6,5 @@ * Side Public License, v 1. */ -import * as vegaLite from 'vega-lite/build-es5/vega-lite'; -import * as vega from 'vega/build-es5/vega'; - -export { vega, vegaLite }; +require('../src/setup_node_env/no_transpilation'); +require('@kbn/dev-utils').shipCiStatsCli(); diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index a4f50e73f1c57..56da185d023a9 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -19,10 +19,7 @@ Definition of done for a feature: - has been verified manually by at least one reviewer - can be used by first & third party plugins - there is no contradiction between client and server API -- works for OSS version - - works with and without a `server.basePath` configured - - cannot crash the Kibana server when it fails -- works for the commercial version with a license +- works with the subscription features - for a logged-in user - for anonymous user - compatible with Spaces diff --git a/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f..a1cb036ce38f8 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -53,7 +53,13 @@ import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; +import { + Plugin, + AsyncPlugin, + PluginInitializer, + PluginInitializerContext, + PluginOpaqueId, +} from './plugins'; import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -304,6 +310,7 @@ export { NotificationsSetup, NotificationsStart, Plugin, + AsyncPlugin, PluginInitializer, PluginInitializerContext, SavedObjectsStart, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index d208ea76c48fe..e47de84ea12b2 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -110,14 +110,14 @@ function pluginInitializerContextMock(config: any = {}) { return mock; } -function createCoreContext(): CoreContext { +function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), env: { mode: { - dev: true, - name: 'development', - prod: false, + dev: !production, + name: production ? 'production' : 'development', + prod: production, }, packageInfo: { version: 'version', diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 76811d4908d22..be805c6a521ce 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -7,6 +7,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, AsyncPlugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index e8e930a5befca..ef919018f120b 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -39,16 +39,16 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if plugin.setup is not a function', async () => { + test('`setup` fails if plugin.setup is not a function', () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); - test('`setup` fails if plugin.start is not a function', async () => { + test('`setup` fails if plugin.start is not a function', () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); @@ -65,8 +65,8 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); - test('`start` fails if setup is not called first', async () => { - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + test('`start` fails if setup is not called first', () => { + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index af95e831a6472..a08a6cf0b431a 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { read } from './plugin_reader'; @@ -23,6 +24,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -40,7 +58,11 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -58,7 +80,9 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -81,10 +105,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { - this.instance = await this.createPluginInstance(); - - return await this.instance.setup(setupContext, plugins); + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { + this.instance = this.createPluginInstance(); + return this.instance.setup(setupContext, plugins); } /** @@ -94,16 +120,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - - this.startDependencies$.next([startContext, plugins, startContract]); - - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** @@ -121,7 +152,7 @@ export class PluginWrapper< this.instance = undefined; } - private async createPluginInstance() { + private createPluginInstance() { const initializer = read(this.name) as PluginInitializer< TSetup, TStart, diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index d44657f9039a3..1f85482569dbc 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -7,9 +7,12 @@ */ import { PluginName } from 'kibana/server'; -import { Plugin } from './plugin'; +import { Plugin, AsyncPlugin } from './plugin'; -export type MockedPluginInitializer = jest.Mock>, any>; +export type MockedPluginInitializer = jest.Mock< + Plugin | AsyncPlugin, + any +>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index a22d48c50247a..e70b78f237d75 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -146,16 +146,16 @@ describe('PluginsService', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` - Map { - Symbol(pluginA) => Array [], - Symbol(pluginB) => Array [ - Symbol(pluginA), - ], - Symbol(pluginC) => Array [ - Symbol(pluginA), - ], - } - `); + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); }); }); @@ -264,7 +264,7 @@ describe('PluginsService', () => { jest.runAllTimers(); // setup plugins await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -344,7 +344,7 @@ describe('PluginsService', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -366,4 +366,124 @@ describe('PluginsService', () => { expect(pluginCInstance.stop).toHaveBeenCalled(); }); }); + + describe('asynchronous plugins', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + const coreContext = coreMock.createCoreContext({ production }); + + const syncPlugin = { id: 'sync-plugin', plugin: createManifest('sync-plugin') }; + mockPluginInitializers.set( + 'sync-plugin', + jest.fn(() => ({ + setup: jest.fn(() => 'setup-sync'), + start: jest.fn(() => 'start-sync'), + stop: jest.fn(), + })) + ); + + const asyncPlugin = { id: 'async-plugin', plugin: createManifest('async-plugin') }; + mockPluginInitializers.set( + 'async-plugin', + jest.fn(() => ({ + setup: jest.fn(() => (asyncSetup ? Promise.resolve('setup-async') : 'setup-sync')), + start: jest.fn(() => (asyncStart ? Promise.resolve('start-async') : 'start-sync')), + stop: jest.fn(), + })) + ); + + const pluginsService = new PluginsService(coreContext, [syncPlugin, asyncPlugin]); + + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 7a10ce1cdfc77..57fbe4cbecd12 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -98,16 +98,29 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); - contracts.set(pluginName, contract); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -132,14 +145,28 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 75ed9aa5f150f..99579ada8ec58 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -194,6 +194,16 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public export interface Capabilities { [key: string]: Record>; @@ -990,15 +1000,15 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382a694bd2e41..6f478004c204e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -235,6 +235,7 @@ export { export { DiscoveredPlugin, Plugin, + AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index dda947972737a..a29fb01fbc009 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,7 +20,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { AsyncPlugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -138,7 +138,7 @@ describe('PluginsService', () => { expect(startDependenciesResolved).toBe(false); return pluginStartContract; }, - } as Plugin); + } as AsyncPlugin); jest.doMock( join(pluginPath, 'server'), diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 68fdfdf62c30b..c90d2e804225c 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -100,7 +100,7 @@ test('`constructor` correctly initializes plugin instance', () => { expect(plugin.optionalPlugins).toEqual(['some-optional-dep']); }); -test('`setup` fails if `plugin` initializer is not exported', async () => { +test('`setup` fails if `plugin` initializer is not exported', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -115,14 +115,14 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Plugin "some-plugin-id" does not export "plugin" definition (plugin-without-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."` ); }); -test('`setup` fails if plugin initializer is not a function', async () => { +test('`setup` fails if plugin initializer is not a function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -137,14 +137,14 @@ test('`setup` fails if plugin initializer is not a function', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Definition of plugin "some-plugin-id" should be a function (plugin-with-wrong-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."` ); }); -test('`setup` fails if initializer does not return object', async () => { +test('`setup` fails if initializer does not return object', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -161,14 +161,14 @@ test('`setup` fails if initializer does not return object', async () => { mockPluginInitializer.mockReturnValue(null); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Initializer for plugin "some-plugin-id" is expected to return plugin instance, but returned "null".]` + ).toThrowErrorMatchingInlineSnapshot( + `"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."` ); }); -test('`setup` fails if object returned from initializer does not define `setup` function', async () => { +test('`setup` fails if object returned from initializer does not define `setup` function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -186,10 +186,10 @@ test('`setup` fails if object returned from initializer does not define `setup` const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Instance of plugin "some-plugin-id" does not define "setup" function.]` + ).toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."` ); }); @@ -223,7 +223,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () expect(mockPluginInstance.setup).toHaveBeenCalledWith(setupContext, setupDependencies); }); -test('`start` fails if setup is not called first', async () => { +test('`start` fails if setup is not called first', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -238,7 +238,7 @@ test('`start` fails if setup is not called first', async () => { ), }); - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 83b3fb53689a7..ca7f11e28de75 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -10,11 +10,13 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { Plugin, + AsyncPlugin, PluginInitializerContext, PluginManifest, PluginInitializer, @@ -49,7 +51,9 @@ export class PluginWrapper< private readonly log: Logger; private readonly initializerContext: PluginInitializerContext; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -83,9 +87,11 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); } @@ -96,14 +102,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart): TStart | Promise { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - this.startDependencies$.next([startContext, plugins, startContract]); - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 1b5994c40c041..5c38deeb5cf6e 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -25,7 +25,6 @@ import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; -const logger = loggingSystemMock.create(); function createPlugin( id: string, { @@ -34,8 +33,8 @@ function createPlugin( server = true, ui = true, }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} -) { - return new PluginWrapper({ +): PluginWrapper { + return new PluginWrapper({ path: 'some-path', manifest: { id, @@ -53,27 +52,27 @@ function createPlugin( }); } +const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); + let pluginsSystem: PluginsSystem; -const configService = configServiceMock.create(); -configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let configService: ReturnType; +let logger: ReturnType; let env: Env; let coreContext: CoreContext; -const setupDeps = coreMock.createInternalSetup(); -const startDeps = coreMock.createInternalStart(); - beforeEach(() => { + logger = loggingSystemMock.create(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); + configService = configServiceMock.create(); + configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); -afterEach(() => { - jest.clearAllMocks(); -}); - test('can be setup even without plugins', async () => { const pluginsSetup = await pluginsSystem.setupPlugins(setupDeps); @@ -208,7 +207,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start start: { 'order-2': 'started-as-2' }, }, ], - ] as Array<[PluginWrapper, Contracts]>); + ] as Array<[PluginWrapper, Contracts]>); const setupContextMap = new Map(); const startContextMap = new Map(); @@ -434,7 +433,7 @@ describe('setup', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + it('throws timeout error if "setup" was not completed in 10 sec.', async () => { const plugin: PluginWrapper = createPlugin('timeout-setup'); jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise((i) => i)); pluginsSystem.addPlugin(plugin); @@ -444,7 +443,7 @@ describe('setup', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -471,8 +470,8 @@ describe('start', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "start" was not completed in 30 sec.', async () => { - const plugin: PluginWrapper = createPlugin('timeout-start'); + it('throws timeout error if "start" was not completed in 10 sec.', async () => { + const plugin = createPlugin('timeout-start'); jest.spyOn(plugin, 'setup').mockResolvedValue({}); jest.spyOn(plugin, 'start').mockImplementation(() => new Promise((i) => i)); @@ -485,7 +484,7 @@ describe('start', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -505,3 +504,120 @@ describe('start', () => { expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`); }); }); + +describe('asynchronous plugins', () => { + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + env = Env.createDefault( + REPO_ROOT, + getEnvOptions({ + cliArgs: { + dev: !production, + envName: production ? 'production' : 'development', + }, + }) + ); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + pluginsSystem = new PluginsSystem(coreContext); + + const syncPlugin = createPlugin('sync-plugin'); + jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); + jest.spyOn(syncPlugin, 'start').mockReturnValue('start-sync'); + pluginsSystem.addPlugin(syncPlugin); + + const asyncPlugin = createPlugin('async-plugin'); + jest + .spyOn(asyncPlugin, 'setup') + .mockReturnValue(asyncSetup ? Promise.resolve('setup-async') : 'setup-sync'); + jest + .spyOn(asyncPlugin, 'start') + .mockReturnValue(asyncStart ? Promise.resolve('start-async') : 'start-sync'); + pluginsSystem.addPlugin(asyncPlugin); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins(startDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1b5e3bbb06e71..b7b8c297ea571 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -94,14 +94,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); @@ -132,14 +143,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 91ccc2dedf272..45db98201b758 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -242,6 +242,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -383,4 +400,8 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index fe2ce76446cb9..b22c326061f66 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -150,12 +150,23 @@ export const removeWriteBlock = ( .catch(catchRetryableEsClientErrors); }; -const waitForIndexStatusGreen = ( +/** + * A yellow index status means the index's primary shard is allocated and the + * index is ready for searching/indexing documents, but ES wasn't able to + * allocate the replicas. When migrations proceed with a yellow index it means + * we don't have as much data-redundancy as we could have, but waiting for + * replicas would mean that v2 migrations fail where v1 migrations would have + * succeeded. It doesn't feel like it's Kibana's job to force users to keep + * their clusters green and even if it's green when we migrate it can turn + * yellow at any point in the future. So ultimately data-redundancy is up to + * users to maintain. + */ +const waitForIndexStatusYellow = ( client: ElasticsearchClient, index: string ): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'green', timeout: '30s' }) + .health({ index, wait_for_status: 'yellow', timeout: '30s' }) .then(() => { return Either.right({}); }) @@ -259,7 +270,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, target), + waitForIndexStatusYellow(client, target), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -687,7 +698,7 @@ export const createIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, indexName), + waitForIndexStatusYellow(client, indexName), TaskEither.map(() => { /** When the index status is 'green' we know that all shards were started */ return 'create_index_succeeded'; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 1bb4e57b0ac29..46cfd935f429b 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -213,12 +213,8 @@ describe('migration actions', () => { } }); it('resolves right if cloning into a new target index', async () => { + const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); expect.assertions(1); - const task = cloneIndex( - client, - 'existing_index_with_write_block', - 'clone_yellow_then_green_index_1' - ); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -229,42 +225,48 @@ describe('migration actions', () => { } `); }); - it('resolves right after waiting for index status to be green if clone target already existed', async () => { + it('resolves right after waiting for index status to be yellow if clone target already existed', async () => { expect.assertions(2); + // Create a yellow index - await client.indices.create({ - index: 'clone_yellow_then_green_index_2', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + await client.indices + .create({ + index: 'clone_red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, - }, - }); + }) + .catch((e) => {}); // Call clone even though the index already exists const cloneIndexPromise = cloneIndex( client, 'existing_index_with_write_block', - 'clone_yellow_then_green_index_2' + 'clone_red_then_yellow_index' )(); - let indexGreen = false; + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ + index: 'clone_red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Enable all shard allocation so that the index status goes yellow + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await cloneIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -278,7 +280,7 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if the source index does not exist', async () => { expect.assertions(1); - const task = cloneIndex(client, 'no_such_index', 'clone_yellow_then_green_index_3'); + const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -674,7 +676,6 @@ describe('migration actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('rejects if there are failures', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_write_block' @@ -689,7 +690,6 @@ describe('migration actions', () => { }); }); it('rejects if there is an error', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'no_such_index' @@ -703,7 +703,6 @@ describe('migration actions', () => { `); }); it('resolves right when successful', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_docs' @@ -722,7 +721,6 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { - expect.assertions(3); // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { dynamic: false as any, @@ -771,7 +769,6 @@ describe('migration actions', () => { describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -793,7 +790,6 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -815,7 +811,6 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -835,7 +830,6 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -858,7 +852,6 @@ describe('migration actions', () => { }); describe('remove_index', () => { it('left index_not_found_exception if index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -877,7 +870,6 @@ describe('migration actions', () => { `); }); it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -899,44 +891,50 @@ describe('migration actions', () => { describe('createIndex', () => { afterAll(async () => { - await client.indices.delete({ index: 'yellow_then_green_index' }); + await client.indices.delete({ index: 'red_then_yellow_index' }); }); - it('resolves right after waiting for an index status to be green if the index already existed', async () => { + it('resolves right after waiting for an index status to be yellow if the index already existed', async () => { expect.assertions(2); - // Create a yellow index - await client.indices.create( - { - index: 'yellow_then_green_index', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + // Create a red index + await client.indices + .create( + { + index: 'red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, }, - }, - { maxRetries: 0 /** handle retry ourselves for now */ } - ); + { maxRetries: 0 /** handle retry ourselves for now */ } + ) + .catch((e) => { + /** ignore */ + }); // Call createIndex even though the index already exists - const createIndexPromise = createIndex(client, 'yellow_then_green_index', undefined as any)(); - let indexGreen = false; + const createIndexPromise = createIndex(client, 'red_then_yellow_index', undefined as any)(); + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ - index: 'yellow_then_green_index', + index: 'red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await createIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -946,7 +944,6 @@ describe('migration actions', () => { }); }); it('rejects when there is an unexpected error creating the index', async () => { - expect.assertions(1); // Creating an index with the same name as an existing alias to induce // failure await expect( @@ -957,7 +954,6 @@ describe('migration actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('resolves right when documents do not yet exist in the index', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, @@ -972,7 +968,6 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { - expect.assertions(1); const existingDocs = ((await searchForOutdatedDocuments( client, 'existing_index_with_docs', @@ -991,7 +986,6 @@ describe('migration actions', () => { `); }); it('rejects if there are errors', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 2d3ab91697e42..317bfe33b3a19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -30,7 +30,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index bce01c93fe886..16ba0c855867c 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -32,7 +32,7 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { adjustTimeout: (t: number) => jest.setTimeout(600000), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 895db80983fc1..5531f847f8bb4 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -182,6 +182,21 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', }; + const mappingsWithUnknownType = { + properties: { + disabled_saved_object_type: { + properties: { + value: { type: 'keyword' }, + }, + }, + }, + _meta: { + migrationMappingPropertyHashes: { + disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, + }; + test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { @@ -189,38 +204,27 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.11.0': {}, }, - mappings: { - properties: { - disabled_saved_object_type: { - properties: { - value: { type: 'keyword' }, - }, - }, - }, - _meta: { - migrationMappingPropertyHashes: { - disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', - }, - }, - }, + mappings: mappingsWithUnknownType, settings: {}, }, }); const newState = model(initState, res); expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + // This snapshot asserts that we merge the + // migrationMappingPropertyHashes of the existing index, but we leave + // the mappings for the disabled_saved_object_type untouched. There + // might be another Kibana instance that knows about this type and + // needs these mappings in place. expect(newState.targetIndexMappings).toMatchInlineSnapshot(` Object { "_meta": Object { "migrationMappingPropertyHashes": Object { + "disabled_saved_object_type": "7997cf5a56cc02bdc9c93361bde732b0", "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", }, }, "properties": Object { - "disabled_saved_object_type": Object { - "dynamic": false, - "properties": Object {}, - }, "new_saved_object_type": Object { "properties": Object { "value": Object { @@ -271,7 +275,7 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.12.0': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_7.11.0_001': { @@ -288,12 +292,37 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.invalid.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); }); test('INIT -> SET_SOURCE_WRITE_BLOCK when migrating from a v2 migrations index (>= 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { aliases: { '.kibana': {}, '.kibana_7.11.0': {} }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_3': { @@ -319,6 +348,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.11.0_001'), targetIndex: '.kibana_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -328,7 +382,7 @@ describe('migrations v2 model', () => { aliases: { '.kibana': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -339,6 +393,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_3'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -346,7 +425,7 @@ describe('migrations v2 model', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana': { aliases: {}, - mappings: { properties: {}, _meta: {} }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -357,6 +436,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_pre6.5.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -366,7 +470,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -386,6 +490,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_3'), targetIndex: 'my-saved-objects_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -395,7 +524,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -416,6 +545,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_7.11.0'), targetIndex: 'my-saved-objects_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index c9a3aa25db4c1..6f915df9dd958 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -60,13 +60,13 @@ function throwBadResponse(state: State, res: any): never { * Merge the _meta.migrationMappingPropertyHashes mappings of an index with * the given target mappings. * - * @remarks Mapping updates are commutative (deeply merged) by Elasticsearch, - * except for the _meta key. The source index we're migrating from might - * contain documents created by a plugin that is disabled in the Kibana - * instance performing this migration. We merge the - * _meta.migrationMappingPropertyHashes mappings from the source index into - * the targetMappings to ensure that any `migrationPropertyHashes` for - * disabled plugins aren't lost. + * @remarks When another instance already completed a migration, the existing + * target index might contain documents and mappings created by a plugin that + * is disabled in the current Kibana instance performing this migration. + * Mapping updates are commutative (deeply merged) by Elasticsearch, except + * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` + * mappings from the existing target index index into the targetMappings we + * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. * * Right now we don't use these `migrationPropertyHashes` but it could be used * in the future to detect if mappings were changed. If mappings weren't @@ -209,7 +209,7 @@ export const model = (currentState: State, resW: ResponseType): // index sourceIndex: Option.none, targetIndex: `${stateP.indexPrefix}_${stateP.kibanaVersion}_001`, - targetIndexMappings: disableUnknownTypeMappingFields( + targetIndexMappings: mergeMigrationMappingPropertyHashes( stateP.targetIndexMappings, indices[aliases[stateP.currentAlias]].mappings ), @@ -242,7 +242,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'SET_SOURCE_WRITE_BLOCK', sourceIndex: Option.some(source) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[source].mappings ), @@ -279,7 +279,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'LEGACY_SET_WRITE_BLOCK', sourceIndex: Option.some(legacyReindexTarget) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[stateP.legacyIndex].mappings ), diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 7574f26979ab1..344a0d151cfb9 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -44,7 +45,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index 2484daf2ea875..3838e4d3b3c8e 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDe ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index 1a717f330d4c2..de47ab9c59611 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -39,7 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index db68b2f87d577..2fa7acfb6cab6 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -43,7 +44,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; const { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index dbbb0faf35c31..609ce2692c777 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 76e422d24732e..fa5517303f18f 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -18,7 +18,7 @@ import { SavedObjectsExportByObjectOptions, SavedObjectsExportError, } from '../export'; -import { validateTypes, validateObjects } from './utils'; +import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -163,7 +163,7 @@ export const registerExportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index b9ad6ce15df2b..6ba23747cf374 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -49,7 +50,7 @@ export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDepen }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const query = req.query; const namespaces = diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index 121cb82155b6e..f28822d95d814 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -25,7 +26,7 @@ export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDepend }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 81220f897f36b..e84c638d3ec99 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -13,7 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -61,7 +61,7 @@ export const registerImportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts index 19c6e3d99d6c2..404074124c92b 100644 --- a/src/core/server/saved_objects/routes/migrate.ts +++ b/src/core/server/saved_objects/routes/migrate.ts @@ -8,6 +8,7 @@ import { IRouter } from '../../http'; import { IKibanaMigrator } from '../migrations'; +import { catchAndReturnBoomErrors } from './utils'; export const registerMigrateRoute = ( router: IRouter, @@ -21,7 +22,7 @@ export const registerMigrateRoute = ( tags: ['access:migrateSavedObjects'], }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const migrator = await migratorPromise; await migrator.runMigrations({ rerun: true }); return res.ok({ diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 682b583f6a791..2a664328d4df2 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -13,8 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; - +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; coreUsageData: CoreUsageDataSetup; @@ -69,7 +68,7 @@ export const registerResolveImportErrorsRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index 857973c5ae006..cb605dac56777 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -38,7 +39,7 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { attributes, version, references } = req.body; const options = { version, references }; diff --git a/src/core/server/saved_objects/routes/utils.test.ts b/src/core/server/saved_objects/routes/utils.test.ts index a24a4a1b51f6a..623d2dcc71fac 100644 --- a/src/core/server/saved_objects/routes/utils.test.ts +++ b/src/core/server/saved_objects/routes/utils.test.ts @@ -9,6 +9,15 @@ import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; +import { catchAndReturnBoomErrors } from './utils'; +import Boom from '@hapi/boom'; +import { + KibanaRequest, + RequestHandler, + RequestHandlerContext, + KibanaResponseFactory, + kibanaResponseFactory, +} from '../../'; async function readStreamToCompletion(stream: Readable) { return createPromiseFromStreams([stream, createConcatStream([])]); @@ -143,3 +152,69 @@ describe('validateObjects', () => { ).toBeUndefined(); }); }); + +describe('catchAndReturnBoomErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); + }; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = catchAndReturnBoomErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); + + it('should re-throw Boom internal/500 errors', async () => { + const handler = createHandler(() => { + throw Boom.internal(); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: Internal Server Error]` + ); + }); +}); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index fc784ac80ed8d..e933badfe80fe 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -7,7 +7,11 @@ */ import { Readable } from 'stream'; -import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; +import { + RequestHandlerWrapper, + SavedObject, + SavedObjectsExportResultDetails, +} from 'src/core/server'; import { createSplitStream, createMapStream, @@ -16,6 +20,7 @@ import { createListStream, createConcatStream, } from '@kbn/utils'; +import Boom from '@hapi/boom'; export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { const savedObjects = await createPromiseFromStreams([ @@ -52,3 +57,30 @@ export function validateObjects( .join(', ')}`; } } + +/** + * Catches errors thrown by saved object route handlers and returns an error + * with the payload and statusCode of the boom error. + * + * This is very close to the core `router.handleLegacyErrors` except that it + * throws internal errors (statusCode: 500) so that the internal error's + * message get logged by Core. + * + * TODO: Remove once https://github.com/elastic/kibana/issues/65291 is fixed. + */ +export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e) && e.output.statusCode !== 500) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers as { [key: string]: string }, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 717fd5fc5ab92..32f12193306e7 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -109,6 +109,27 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); + it('if saved objects index does not exist makes NotFound a SavedObjectsClient/generalError', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { + error: { + reason: + 'no such index [.kibana_8.0.0] and [require_alias] request flag is [true] and [.kibana_8.0.0] is not an alias', + }, + }, + }) + ); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(false); + const genericError = decorateEsError(error); + expect(genericError.message).toEqual( + `Saved object index alias [.kibana_8.0.0] not found: Response Error` + ); + expect(genericError.output.statusCode).toBe(500); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(true); + }); + it('makes BadRequest a SavedObjectsClient/BadRequest error', () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ statusCode: 400 }) diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index 59a9210ff5130..e1aa1ab2f956d 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -63,6 +63,12 @@ export function decorateEsError(error: EsErrors) { } if (responseErrors.isNotFound(error.statusCode)) { + const match = error?.meta?.body?.error?.reason?.match( + /no such index \[(.+)\] and \[require_alias\] request flag is \[true\] and \[.+\] is not an alias/ + ); + if (match?.length > 0) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(error, match[1]); + } return SavedObjectsErrorHelpers.createGenericNotFoundError(); } diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 2495679a2f8c2..581145c7c09d1 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -135,6 +135,19 @@ export class SavedObjectsErrorHelpers { return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); } + public static createIndexAliasNotFoundError(alias: string) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(Boom.internal(), alias); + } + + public static decorateIndexAliasNotFoundError(error: Error, alias: string) { + return decorate( + error, + CODE_GENERAL_ERROR, + 500, + `Saved object index alias [${alias}] not found` + ); + } + public static isNotFoundError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; } @@ -185,4 +198,8 @@ export class SavedObjectsErrorHelpers { public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } + + public static isGeneralError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; + } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 0a1c18c01ad82..aac508fb5b909 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; +import { errors as EsErrors } from '@elastic/elasticsearch'; const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -4341,8 +4342,14 @@ describe('SavedObjectsRepository', () => { }); it(`throws when ES is unable to find the document during update`, async () => { + const notFoundError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) ); await expectNotFoundError(type, id); expect(client.update).toHaveBeenCalledTimes(1); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index a662a374b063e..fcd72aa4326a2 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -299,6 +299,7 @@ export class SavedObjectsRepository { refresh, body: raw._source, ...(overwrite && version ? decodeRequestVersion(version) : {}), + require_alias: true, }; const { body } = @@ -469,6 +470,7 @@ export class SavedObjectsRepository { const bulkResponse = bulkCreateParams.length ? await this.client.bulk({ refresh, + require_alias: true, body: bulkCreateParams, }) : undefined; @@ -1117,8 +1119,8 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; - const { body, statusCode } = await this.client.update( - { + const { body } = await this.client + .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...getExpectedVersionProperties(version, preflightResult), @@ -1128,14 +1130,15 @@ export class SavedObjectsRepository { doc, }, _source_includes: ['namespace', 'namespaces', 'originId'], - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } + require_alias: true, + }) + .catch((err) => { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + throw err; + }); const { originId } = body.get._source; let namespaces = []; @@ -1496,6 +1499,7 @@ export class SavedObjectsRepository { refresh, body: bulkUpdateParams, _source_includes: ['originId'], + require_alias: true, }) : undefined; @@ -1712,6 +1716,7 @@ export class SavedObjectsRepository { id: raw._id, index: this.getIndexForType(type), refresh, + require_alias: true, _source: 'true', body: { script: { @@ -1933,12 +1938,18 @@ export class SavedObjectsRepository { } } -function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { +function getBulkOperationError( + error: { type: string; reason?: string; index?: string }, + type: string, + id: string +) { switch (error.type) { case 'version_conflict_engine_exception': return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); case 'document_missing_exception': return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + case 'index_not_found_exception': + return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); default: return { message: error.reason || JSON.stringify(error), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 40a12290be31b..09207608908a4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -203,6 +203,16 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -1815,9 +1825,9 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } @@ -1836,7 +1846,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -2335,6 +2345,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createIndexAliasNotFoundError(alias: string): DecoratedError; + // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; // (undocumented) static createTooManyRequestsError(type: string, id: string): DecoratedError; @@ -2353,6 +2365,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateGeneralError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; + // (undocumented) static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; @@ -2369,6 +2383,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; // (undocumented) + static isGeneralError(error: Error | DecoratedError): boolean; + // (undocumented) static isInvalidVersionError(error: Error | DecoratedError): boolean; // (undocumented) static isNotAuthorizedError(error: Error | DecoratedError): boolean; @@ -3135,9 +3151,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index b02f2ec9c7610..86a9a24fab6de 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docExistsSuite() { +export const docExistsSuite = (savedObjectsIndex: string) => () => { async function setup(options: any = {}) { const { initialSettings } = options; @@ -16,7 +16,7 @@ export function docExistsSuite() { // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { conflicts: 'proceed', query: { match_all: {} }, @@ -212,4 +212,4 @@ export function docExistsSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index ef3b3928e0d9c..9fa3e4c1cfe78 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingSuite() { +export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingSuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -136,4 +136,4 @@ export function docMissingSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts index f3a02cfe176e9..78fdab7eb8c5d 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingAndIndexReadOnlySuite() { +export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingAndIndexReadOnlySuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -30,7 +30,7 @@ export function docMissingAndIndexReadOnlySuite() { // set the index to read only await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -42,11 +42,11 @@ export function docMissingAndIndexReadOnlySuite() { }); afterEach(async () => { - const { kbnServer, callCluster } = getServices(); + const { callCluster } = getServices(); // disable the read only block await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -142,4 +142,4 @@ export function docMissingAndIndexReadOnlySuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 184c75d88f3b8..6e6c357e6cccc 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -6,20 +6,25 @@ * Side Public License, v 1. */ +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; - import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +const savedObjectIndex = `.kibana_${kibanaVersion}_001`; + describe('uiSettings/routes', function () { jest.setTimeout(10000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ - describe('doc missing', docMissingSuite); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite); - describe('doc exists', docExistsSuite); + describe('doc missing', docMissingSuite(savedObjectIndex)); + describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); + describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); }); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 1bea45da51af9..87176bed5de11 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -37,9 +37,6 @@ export async function startServers() { adjustTimeout: (t) => jest.setTimeout(t), settings: { kbn: { - migrations: { - enableV2: false, - }, uiSettings: { overrides: { foo: 'bar', diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index cf5589fecdf43..14f614643ac9f 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -40,7 +40,7 @@ const DEFAULTS_SETTINGS = { }, logging: { silent: true }, plugins: {}, - migrations: { skip: true }, + migrations: { skip: false }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { @@ -185,7 +185,7 @@ export function createTestServers({ adjustTimeout: (timeout: number) => void; settings?: { es?: { - license: 'oss' | 'basic' | 'gold' | 'trial'; + license: 'basic' | 'gold' | 'trial'; [key: string]: any; }; kbn?: { @@ -208,7 +208,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'basic'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js new file mode 100644 index 0000000000000..fe60f9176d243 --- /dev/null +++ b/src/dev/bazel_workspace_status.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +// This script will be run bazel when building process starts to +// generate key-value information that represents the status of the +// workspace. The output should be like +// +// KEY1 VALUE1 +// KEY2 VALUE2 +// +// If the script exits with non-zero code, it's considered as a failure +// and the output will be discarded. + +(async () => { + const execa = require('execa'); + const os = require('os'); + + async function runCmd(cmd, args) { + try { + return await execa(cmd, args); + } catch (e) { + return { exitCode: 1 }; + } + } + + // Git repo + const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; + const repoUrlCmdResult = await runCmd('git', [ + 'config', + '--get', + `remote.${kbnGitOriginName}.url`, + ]); + if (repoUrlCmdResult.exitCode === 0) { + // Only output REPO_URL when found it + console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); + } + + // Commit SHA + const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + if (commitSHACmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + + // Git branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } + + // Host + if (process.env.CI) { + const hostCmdResult = await runCmd('hostname'); + const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); + const coresStr = os.cpus().filter((cpu, index) => { + return !cpu.model.includes('Intel') || index % 2 === 1; + }).length; + + if (hostCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`HOST ${hostStr}-${coresStr}`); + } +})(); diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index 91fad2ca52617..d2d2d3275270b 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -6,20 +6,18 @@ * Side Public License, v 1. */ +import Path from 'path'; + import { REPO_ROOT } from '@kbn/utils'; -import { CiStatsReporter } from '@kbn/dev-utils'; -import { - runOptimizer, - OptimizerConfig, - logOptimizerState, - reportOptimizerStats, -} from '@kbn/optimizer'; +import { lastValueFrom } from '@kbn/std'; +import { CiStatsMetrics } from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; -import { Task } from '../lib'; +import { Task, deleteAll, write, read } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(_, log, build) { + async run(buildConfig, log, build) { const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, outputRoot: build.resolvePath(), @@ -31,12 +29,27 @@ export const BuildKibanaPlatformPlugins: Task = { includeCoreBundle: true, }); - const reporter = CiStatsReporter.fromEnv(log); + await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + + const combinedMetrics: CiStatsMetrics = []; + const metricFilePaths: string[] = []; + for (const bundle of config.bundles) { + const path = Path.resolve(bundle.outputDir, 'metrics.json'); + const metrics: CiStatsMetrics = JSON.parse(await read(path)); + combinedMetrics.push(...metrics); + metricFilePaths.push(path); + } + + // write combined metrics to target + await write( + buildConfig.resolveFromTarget('optimizer_bundle_metrics.json'), + JSON.stringify(combinedMetrics, null, 2) + ); - await runOptimizer(config) - .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) - .toPromise(); + // delete all metric files + await deleteAll(metricFilePaths, log); + // delete all bundle cache files await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci index 5b345d3c9e207..ef6fab3a30590 100644 --- a/src/dev/ci_setup/.bazelrc-ci +++ b/src/dev/ci_setup/.bazelrc-ci @@ -5,6 +5,12 @@ # Import and load bazelrc common settings for ci env try-import %workspace%/src/dev/ci_setup/.bazelrc-ci.common -# Remote cache settings for ci env -# build --google_default_credentials -# build --remote_upload_local_results=true +# BuildBuddy settings +## Remote settings including cache +build --bes_results_url=https://app.buildbuddy.io/invocation/ +build --bes_backend=grpcs://cloud.buildbuddy.io +build --remote_cache=grpcs://cloud.buildbuddy.io +build --remote_timeout=3600 + +## Metadata settings +build --build_metadata=ROLE=CI diff --git a/src/dev/ci_setup/.bazelrc-ci.common b/src/dev/ci_setup/.bazelrc-ci.common index 3f58e4e03a178..9d00ee5639741 100644 --- a/src/dev/ci_setup/.bazelrc-ci.common +++ b/src/dev/ci_setup/.bazelrc-ci.common @@ -4,8 +4,5 @@ # Don't be spammy in the logs build --noshow_progress -# Print all the options that apply to the build. -build --announce_rc - # More details on failures build --verbose_failures=true diff --git a/src/dev/ci_setup/load_env_keys.sh b/src/dev/ci_setup/load_env_keys.sh index 62d29db232eae..5f7a6c26bab21 100644 --- a/src/dev/ci_setup/load_env_keys.sh +++ b/src/dev/ci_setup/load_env_keys.sh @@ -34,6 +34,9 @@ else PERCY_TOKEN=$(retry 5 vault read -field=value secret/kibana-issues/dev/percy) export PERCY_TOKEN + KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) + export KIBANA_BUILDBUDDY_CI_API_KEY + # remove vault related secrets unset VAULT_ROLE_ID VAULT_SECRET_ID VAULT_TOKEN VAULT_ADDR fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index e5e21e312b0dd..db7110d2d0875 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,6 +10,17 @@ echo " -- PARENT_DIR='$PARENT_DIR'" echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'" echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + ### ### install dependencies ### @@ -21,8 +32,6 @@ yarn kbn bootstrap ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - ### ### verify no git modifications @@ -65,8 +74,3 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi - -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5dac270239c4a..2deafaaf35a94 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -175,4 +175,24 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### remove write permissions on buildbuddy remote cache for prs +### +if [[ "$ghprbPullId" ]] ; then + echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" + echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" + echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" +fi + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + export CI_ENV_SETUP=true diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da5024..e504d5f0b9a9f 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -8,7 +8,6 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { APMOSSConfig } from './'; import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; import { tutorialProvider } from './tutorial'; @@ -17,10 +16,10 @@ export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { const config$ = this.initContext.config.create(); - const config = await config$.pipe(take(1)).toPromise(); + const config = this.initContext.config.get(); const apmTutorialProvider = tutorialProvider({ indexPatternTitle: config.indexPattern, @@ -35,6 +34,7 @@ export class APMOSSPlugin implements Plugin { plugins.home.tutorials.registerTutorial(apmTutorialProvider); return { + config, config$, getRegisteredTutorialProvider: () => apmTutorialProvider, }; @@ -45,6 +45,7 @@ export class APMOSSPlugin implements Plugin { } export interface APMOSSPluginSetup { + config: APMOSSConfig; config$: Observable; getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index b2f43b315aa9b..a5f1ca6107600 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { ProxyConfigCollection } from './lib'; @@ -28,7 +27,7 @@ export class ConsoleServerPlugin implements Plugin { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -36,8 +35,8 @@ export class ConsoleServerPlugin implements Plugin { }, })); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.ctx.config.get(); + const globalConfig = this.ctx.config.legacy.get(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 5d384ed8ebd82..ef730e16bc5cf 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -22,7 +22,7 @@ import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; -export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; +export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; export interface AddToLibraryActionContext { embeddable: IEmbeddable; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index b27322b6bec53..d12fea07bdd41 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -21,7 +21,7 @@ import { import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashboard_app_functions'; -import { DashboardContainer, DashboardContainerInput } from '../..'; +import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; @@ -68,7 +68,9 @@ export const useDashboardContainer = ( searchSession.restore(searchSessionIdFromURL); } - const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + const incomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts new file mode 100644 index 0000000000000..ffdd47e5cdf49 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternsFetcher } from '.'; +import { ElasticsearchClient } from 'kibana/server'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; + +describe('Index Pattern Fetcher - server', () => { + let indexPatterns: IndexPatternsFetcher; + let esClient: ElasticsearchClient; + const emptyResponse = { + body: { + count: 0, + }, + }; + const response = { + body: { + count: 1115, + }, + }; + const patternList = ['a', 'b', 'c']; + beforeEach(() => { + esClient = ({ + count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + }); + + it('Removes pattern without matching indices', async () => { + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(['b', 'c']); + }); + + it('Returns all patterns when all match indices', async () => { + esClient = ({ + count: jest.fn().mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(patternList); + }); + it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + class ServerError extends Error { + public body?: Record; + constructor( + message: string, + public readonly statusCode: number, + errBody?: Record + ) { + super(message); + this.body = errBody; + } + } + + esClient = ({ + count: jest + .fn() + .mockResolvedValueOnce(response) + .mockRejectedValue( + new ServerError('index_not_found_exception', 404, indexNotFoundException) + ), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual([patternList[0]]); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index cc8bfe28bbc9a..3acdde33f599e 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -58,9 +58,16 @@ export class IndexPatternsFetcher { rollupIndex?: string; }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; + const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + let patternListActive: string[] = patternList; + // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless + if (patternList.length > 1) { + patternListActive = await this.validatePatternListActive(patternList); + } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - pattern, + // if none of the patterns are active, pass the original list to get an error + patternListActive.length > 0 ? patternListActive : patternList, metaFields, { allow_no_indices: fieldCapsOptions @@ -68,6 +75,7 @@ export class IndexPatternsFetcher { : this.allowNoIndices, } ); + if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -118,4 +126,34 @@ export class IndexPatternsFetcher { } return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } + + /** + * Returns an index pattern list of only those index pattern strings in the given list that return indices + * + * @param patternList string[] + * @return {Promise} + */ + async validatePatternListActive(patternList: string[]) { + const result = await Promise.all( + patternList + .map((pattern) => + this.elasticsearchClient.count({ + index: pattern, + }) + ) + .map((p) => + p.catch((e) => { + if (e.body.error.type === 'index_not_found_exception') { + return { body: { count: 0 } }; + } + throw e; + }) + ) + ); + return result.reduce( + (acc: string[], { body: { count } }, patternListIndex) => + count > 0 ? [...acc, patternList[patternListIndex]] : acc, + [] + ); + } } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 68582a9d877e9..3b1440f211bfe 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -885,6 +885,7 @@ export class IndexPatternsFetcher { type?: string; rollupIndex?: string; }): Promise; + validatePatternListActive(patternList: string[]): Promise; } // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b22bb6dc71342..af63485507d05 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -22,7 +22,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; @@ -43,13 +42,9 @@ import { setBreadcrumbsTitle, } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { popularizeField } from '../helpers/popularize_field'; -import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; -import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, @@ -69,12 +64,10 @@ const { chrome, data, history: getHistory, - indexPatterns, filterManager, timefilter, toastNotifications, uiSettings: config, - trackUiMetric, } = getServices(); const fetchStatuses = { @@ -292,21 +285,6 @@ function discoverController($route, $scope, Promise) { } ); - $scope.setIndexPattern = async (id) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern) { - const nextAppState = getSwitchIndexPatternAppState( - $scope.indexPattern, - nextIndexPattern, - $scope.state.columns, - $scope.state.sort, - config.get(MODIFY_COLUMNS_ON_SWITCH), - $scope.useNewFieldsApi - ); - await setAppState(nextAppState); - } - }; - // update data source when filters update subscriptions.add( subscribeWithScope( @@ -327,6 +305,7 @@ function discoverController($route, $scope, Promise) { sampleSize: config.get(SAMPLE_SIZE_SETTING), timefield: getTimeField(), savedSearch: savedSearch, + services, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), @@ -340,18 +319,8 @@ function discoverController($route, $scope, Promise) { requests: new RequestAdapter(), }); - $scope.timefilterUpdateHandler = (ranges) => { - timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.showSaveQuery = capabilities.discover.saveQuery; - $scope.showTimeCol = - !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -495,12 +464,6 @@ function discoverController($route, $scope, Promise) { ) ); - $scope.changeInterval = (interval) => { - if (interval) { - setAppState({ interval }); - } - }; - $scope.$watchMulti( ['rows', 'fetchStatus'], (function updateResultState() { @@ -606,19 +569,6 @@ function discoverController($route, $scope, Promise) { } }; - $scope.updateSavedQueryId = (newSavedQueryId) => { - if (newSavedQueryId) { - setAppState({ savedQuery: newSavedQueryId }); - } else { - // remove savedQueryId from state - const state = { - ...appStateContainer.getState(), - }; - delete state.savedQuery; - appStateContainer.set(state); - } - }; - function getDimensions(aggs, timeRange) { const [metric, agg] = aggs; agg.params.timeRange = timeRange; @@ -752,65 +702,6 @@ function discoverController($route, $scope, Promise) { return Promise.resolve(); }; - $scope.setSortOrder = function setSortOrder(sort) { - setAppState({ sort }); - }; - - // TODO: On array fields, negating does not negate the combination, rather all terms - $scope.filterQuery = function (field, values, operation) { - const { indexPattern } = $scope; - - popularizeField(indexPattern, field.name, indexPatterns); - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - $scope.indexPattern.id - ); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); - } - return filterManager.addFilters(newFilters); - }; - - $scope.addColumn = function addColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi); - setAppState({ columns }); - }; - - $scope.removeColumn = function removeColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi); - // The state's sort property is an array of [sortByColumn,sortDirection] - const sort = $scope.state.sort.length - ? $scope.state.sort.filter((subArr) => subArr[0] !== columnName) - : []; - setAppState({ columns, sort }); - }; - - $scope.moveColumn = function moveColumn(columnName, newIndex) { - const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); - setAppState({ columns }); - }; - - $scope.setColumns = function setColumns(columns) { - // remove first element of columns if it's the configured timeFieldName, which is prepended automatically - const actualColumns = - $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] - ? columns.slice(1) - : columns; - $scope.state = { ...$scope.state, columns: actualColumns }; - setAppState({ columns: actualColumns }); - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 83a9cf23c85f3..dc18b7929318b 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -8,27 +8,16 @@ hits="hits" index-pattern="indexPattern" minimum-visible-rows="minimumVisibleRows" - on-add-column="addColumn" - on-add-filter="filterQuery" - on-move-column="moveColumn" - on-change-interval="changeInterval" - on-remove-column="removeColumn" - on-set-columns="setColumns" on-skip-bottom-button-click="onSkipBottomButtonClick" - on-sort="setSortOrder" opts="opts" reset-query="resetQuery" result-state="resultState" rows="rows" search-source="searchSource" - set-index-pattern="setIndexPattern" - show-save-query="showSaveQuery" state="state" - time-filter-update-handler="timefilterUpdateHandler" time-range="timeRange" top-nav-menu="topNavMenu" update-query="handleRefresh" - update-saved-query-id="updateSavedQueryId" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" > diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 946f11024360f..53ced59b17c5d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -5,6 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; +import { popularizeField } from '../../../helpers/popularize_field'; +import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { AppState } from '../../discover_state'; /** * Helper function to provide a fallback to a single _source column if the given array of columns @@ -47,3 +51,60 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb modifiedColumns.splice(newIndex, 0, columnName); // insert before new index return modifiedColumns; } + +export function getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + useNewFieldsApi, + setAppState, + state, +}: { + capabilities: Capabilities; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: (state: Partial) => void; + state: AppState; +}) { + function onAddColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); + setAppState({ columns }); + } + + function onRemoveColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); + // The state's sort property is an array of [sortByColumn,sortDirection] + const sort = + state.sort && state.sort.length + ? state.sort.filter((subArr) => subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); + } + + function onMoveColumn(columnName: string, newIndex: number) { + const columns = moveColumn(state.columns || [], columnName, newIndex); + setAppState({ columns }); + } + + function onSetColumns(columns: string[]) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + setAppState({ columns: actualColumns }); + } + return { + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +} diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 2a88c1b713132..8d1360aeaddad 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { Discover } from './discover'; export function createDiscoverDirective(reactDirective: any) { @@ -18,24 +17,15 @@ export function createDiscoverDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['onSetColumns', { watchDepth: 'reference' }], ['onSkipBottomButtonClick', { watchDepth: 'reference' }], - ['onSort', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], - ['timefilterUpdateHandler', { watchDepth: 'reference' }], ['timeRange', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index bb0014f4278a1..f0f11558abd65 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { Discover } from './discover'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; import { GetStateReturn } from '../angular/discover_state'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; @@ -46,7 +47,14 @@ jest.mock('../../kibana_services', () => { function getProps(indexPattern: IndexPattern): DiscoverProps { const searchSourceMock = createSearchSourceMock({}); - const state = ({} as unknown) as GetStateReturn; + const services = ({ + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; return { fetch: jest.fn(), @@ -56,14 +64,7 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { hits: esHits.length, indexPattern, minimumVisibleRows: 10, - onAddColumn: jest.fn(), - onAddFilter: jest.fn(), - onChangeInterval: jest.fn(), - onMoveColumn: jest.fn(), - onRemoveColumn: jest.fn(), - onSetColumns: jest.fn(), onSkipBottomButtonClick: jest.fn(), - onSort: jest.fn(), opts: { config: mockUiSettings, data: dataPluginMock.createStartContract(), @@ -74,20 +75,18 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { navigateTo: jest.fn(), sampleSize: 10, savedSearch: savedSearchMock, - setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), - stateContainer: state, timefield: indexPattern.timeFieldName || '', + setAppState: jest.fn(), + services, + stateContainer: {} as GetStateReturn, }, resetQuery: jest.fn(), resultState: 'ready', rows: esHits, searchSource: searchSourceMock, - setIndexPattern: jest.fn(), state: { columns: [] }, - timefilterUpdateHandler: jest.fn(), updateQuery: jest.fn(), - updateSavedQueryId: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index baee0623f0b5a..99baa30e18c7a 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -5,9 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import './discover.scss'; -import React, { useState, useRef, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -21,37 +20,34 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { getServices } from '../../kibana_services'; import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; -import { search } from '../../../../data/public'; -import { - DiscoverSidebarResponsive, - DiscoverSidebarResponsiveProps, -} from './sidebar/discover_sidebar_responsive'; +import { esFilters, IndexPatternField, search } from '../../../../data/public'; +import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { popularizeField } from '../helpers/popularize_field'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { DocViewFilterFn } from '../doc_views/doc_views_types'; +import { DiscoverGrid } from './discover_grid/discover_grid'; +import { DiscoverTopNav } from './discover_topnav'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( - -)); -const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( - -)); - -const DataGridMemoized = React.memo((props: DiscoverGridProps) => ); +const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const SidebarMemoized = React.memo(DiscoverSidebarResponsive); +const DataGridMemoized = React.memo(DiscoverGrid); +const TopNavMemoized = React.memo(DiscoverTopNav); export function Discover({ fetch, @@ -62,25 +58,15 @@ export function Discover({ hits, indexPattern, minimumVisibleRows, - onAddColumn, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSetColumns, onSkipBottomButtonClick, - onSort, opts, resetQuery, resultState, rows, searchSource, - setIndexPattern, state, - timefilterUpdateHandler, timeRange, updateQuery, - updateSavedQueryId, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -92,28 +78,9 @@ export function Discover({ }; const [toggleOn, toggleChart] = useState(true); + const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; + const { trackUiMetric, capabilities, indexPatterns } = services; const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = useMemo(() => getServices(), []); - const topNavMenu = useMemo( - () => - getTopNavLinks({ - getFieldCounts: opts.getFieldCounts, - indexPattern, - inspectorAdapters: opts.inspectorAdapters, - navigateTo: opts.navigateTo, - savedSearch: opts.savedSearch, - services, - state: opts.stateContainer, - onOpenInspector: () => { - // prevent overlapping - setExpandedDoc(undefined); - }, - }), - [indexPattern, opts, services] - ); - const { TopNavMenu } = services.navigation.ui; - const { trackUiMetric } = services; - const { savedSearch, indexPatternList, config } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) @@ -123,6 +90,95 @@ export function Discover({ const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( + () => + getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const onOpenInspector = useCallback(() => { + // prevent overlapping + setExpandedDoc(undefined); + }, [setExpandedDoc]); + + const onSort = useCallback( + (sort: string[][]) => { + setAppState({ sort }); + }, + [setAppState] + ); + + const onAddFilter = useCallback( + (field: IndexPatternField | string, values: string, operation: '+' | '-') => { + const fieldName = typeof field === 'string' ? field : field.name; + popularizeField(indexPattern, fieldName, indexPatterns); + const newFilters = esFilters.generateFilters( + opts.filterManager, + field, + values, + operation, + String(indexPattern.id) + ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } + return opts.filterManager.addFilters(newFilters); + }, + [opts, indexPattern, indexPatterns, trackUiMetric] + ); + + const onChangeInterval = useCallback( + (interval: string) => { + if (interval) { + setAppState({ interval }); + } + }, + [setAppState] + ); + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBackToTop = useCallback(() => { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, [scrollableDesktop]); + + const onResize = useCallback( + (colSettings: { columnId: string; width: number }) => { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }, + [opts, state] + ); + const columns = useMemo(() => { if (!state.columns) { return []; @@ -132,20 +188,12 @@ export function Discover({ return ( -

@@ -154,16 +202,19 @@ export function Discover({ { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }} + onBackToTop={onBackToTop} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} @@ -352,19 +393,11 @@ export function Discover({ services={services} settings={state.grid} onAddColumn={onAddColumn} - onFilter={onAddFilter} + onFilter={onAddFilter as DocViewFilterFn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} - onResize={(colSettings: { columnId: string; width: number }) => { - const grid = { ...state.grid } || {}; - const newColumns = { ...grid.columns } || {}; - newColumns[colSettings.columnId] = { - width: colSettings.width, - }; - const newGrid = { ...grid, columns: newColumns }; - opts.setAppState({ grid: newGrid }); - }} + onResize={onResize} /> )} diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx new file mode 100644 index 0000000000000..3f12386281059 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; +import { AppState, GetStateReturn } from '../angular/discover_state'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; +import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; +import { IndexPatternAttributes } from '../../../../data/common/index_patterns'; +import { SavedObject } from '../../../../../core/types'; +import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; +import { RequestAdapter } from '../../../../inspector/common/adapters/request'; +import { TopNavMenu } from '../../../../navigation/public'; + +function getProps(): DiscoverTopNavProps { + const state = ({} as unknown) as AppState; + const services = ({ + navigation: { + ui: { TopNavMenu }, + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; + const indexPattern = indexPatternMock; + return { + indexPattern: indexPatternMock, + opts: { + config: mockUiSettings, + data: dataPluginMock.createStartContract(), + filterManager: createFilterManagerMock(), + getFieldCounts: jest.fn(), + indexPatternList: (indexPattern as unknown) as Array>, + inspectorAdapters: { requests: {} as RequestAdapter }, + navigateTo: jest.fn(), + sampleSize: 10, + savedSearch: savedSearchMock, + services, + setAppState: jest.fn(), + setHeaderActionMenu: jest.fn(), + stateContainer: {} as GetStateReturn, + timefield: indexPattern.timeFieldName || '', + }, + state, + updateQuery: jest.fn(), + onOpenInspector: jest.fn(), + }; +} + +describe('Discover topnav component', () => { + test('setHeaderActionMenu was called', () => { + const props = getProps(); + mountWithIntl(); + expect(props.opts.setHeaderActionMenu).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx new file mode 100644 index 0000000000000..69a1433b6505c --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -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 and the Server Side Public License, v 1; you may not 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, { useMemo } from 'react'; +import { DiscoverProps } from './types'; +import { getTopNavLinks } from './top_nav/get_top_nav_links'; + +export type DiscoverTopNavProps = Pick< + DiscoverProps, + 'indexPattern' | 'updateQuery' | 'state' | 'opts' +> & { onOpenInspector: () => void }; + +export const DiscoverTopNav = ({ + indexPattern, + opts, + onOpenInspector, + state, + updateQuery, +}: DiscoverTopNavProps) => { + const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); + const { TopNavMenu } = opts.services.navigation.ui; + const topNavMenu = useMemo( + () => + getTopNavLinks({ + getFieldCounts: opts.getFieldCounts, + indexPattern, + inspectorAdapters: opts.inspectorAdapters, + navigateTo: opts.navigateTo, + savedSearch: opts.savedSearch, + services: opts.services, + state: opts.stateContainer, + onOpenInspector, + }), + [indexPattern, opts, onOpenInspector] + ); + + const updateSavedQueryId = (newSavedQueryId: string | undefined) => { + const { appStateContainer, setAppState } = opts.stateContainer; + if (newSavedQueryId) { + setAppState({ savedQuery: newSavedQueryId }); + } else { + // remove savedQueryId from state + const newState = { + ...appStateContainer.getState(), + }; + delete newState.savedQuery; + appStateContainer.set(newState); + } + }; + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 7178eccfec4b6..73de3b14f88f6 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -7,39 +7,44 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; - -// @ts-ignore import { ShallowWrapper } from 'enzyme'; import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; -import { DiscoverIndexPattern } from './discover_index_pattern'; +import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const indexPattern = { - id: 'test1', + id: 'the-index-pattern-id-first', title: 'test1 title', -} as IIndexPattern; +} as IndexPattern; const indexPattern1 = { - id: 'test1', + id: 'the-index-pattern-id-first', attributes: { title: 'test1 title', }, } as SavedObject; const indexPattern2 = { - id: 'test2', + id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, } as SavedObject; const defaultProps = { + config: configMock, indexPatternList: [indexPattern1, indexPattern2], selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(async () => {}), + state: {}, + setAppState: jest.fn(), + useNewFieldsApi: true, + indexPatterns: indexPatternsMock, }; function getIndexPatternPickerList(instance: ShallowWrapper) { @@ -63,11 +68,11 @@ function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: describe('DiscoverIndexPattern', () => { test('Invalid props dont cause an exception', () => { - const props = { + const props = ({ indexPatternList: null, selectedIndexPattern: null, setIndexPattern: jest.fn(), - } as any; + } as unknown) as DiscoverIndexPatternProps; expect(shallow()).toMatchSnapshot(`""`); }); @@ -80,10 +85,15 @@ describe('DiscoverIndexPattern', () => { ]); }); - test('should switch data panel to target index pattern', () => { + test('should switch data panel to target index pattern', async () => { const instance = shallow(); - - selectIndexPatternPickerOption(instance, 'test2 title'); - expect(defaultProps.setIndexPattern).toHaveBeenCalledWith('test2'); + await act(async () => { + selectIndexPatternPickerOption(instance, 'test2 title'); + }); + expect(defaultProps.setAppState).toHaveBeenCalledWith({ + index: 'the-index-pattern-id', + columns: [], + sort: [], + }); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 29c62d5c60775..ea3e35f607be4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -6,35 +6,63 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; -import { SavedObject } from 'kibana/public'; -import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; +import React, { useState, useEffect, useCallback } from 'react'; +import { IUiSettingsClient, SavedObject } from 'kibana/public'; +import { + IndexPattern, + IndexPatternAttributes, + IndexPatternsContract, +} from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverIndexPatternProps { + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * list of available index patterns, if length > 1, component offers a "change" link */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: IIndexPattern; + selectedIndexPattern: IndexPattern; + /** + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state + */ + state: AppState; /** - * triggered when user selects a new index pattern + * Read from the Fields API */ - setIndexPattern: (id: string) => void; + useNewFieldsApi?: boolean; } /** * Component allows you to select an index pattern in discovers side bar */ export function DiscoverIndexPattern({ + config, indexPatternList, selectedIndexPattern, - setIndexPattern, + indexPatterns, + state, + setAppState, + useNewFieldsApi, }: DiscoverIndexPatternProps) { const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ id: entity.id, @@ -42,6 +70,24 @@ export function DiscoverIndexPattern({ })); const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; + const setIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && selectedIndexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + selectedIndexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + useNewFieldsApi + ); + setAppState(nextAppState); + } + }, + [selectedIndexPattern, state, config, indexPatterns, setAppState, useNewFieldsApi] + ); + const [selected, setSelected] = useState({ id: selectedId, title: selectedTitle || '', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 9c33bbcbc200a..0ff70585af144 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -24,6 +24,8 @@ import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const mockServices = ({ history: () => ({ @@ -56,7 +58,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -84,20 +86,22 @@ function getCompProps() { } } return { + config: configMock, columns: ['extension'], fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), + setAppState: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index db5f40d8e13cb..f0303553dfac0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -9,7 +9,6 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { UiCounterMetricType } from '@kbn/analytics'; import { EuiAccordion, EuiFlexItem, @@ -25,122 +24,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -export interface DiscoverSidebarProps { - /** - * Determines whether add/remove buttons are displayed not only when focused - */ - alwaysShowActionButtons?: boolean; - /** - * the selected columns displayed in the doc table in discover - */ - columns: string[]; - /** - * a statistics of the distribution of fields in the given hits - */ - fieldCounts: Record; +export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** * Current state of the field filter, filtering fields by name, type, ... */ fieldFilter: FieldFilterState; - /** - * hits fetched from ES, displayed in the doc table - */ - hits: ElasticSearchHit[]; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Callback function when selecting a field - */ - onAddField: (fieldName: string) => void; - /** - * Callback function when adding a filter from sidebar - */ - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - /** - * Callback function when removing a field - * @param fieldName - */ - onRemoveField: (fieldName: string) => void; - /** - * Currently selected index pattern - */ - selectedIndexPattern?: IndexPattern; - /** - * Discover plugin services; - */ - services: DiscoverServices; /** * Change current state of fieldFilter */ setFieldFilter: (next: FieldFilterState) => void; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; - /** - * If on, fields are read from the fields API, not from source - */ - useNewFieldsApi?: boolean; - /** - * Metric tracking function - * @param metricType - * @param eventName - */ - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; - - /** - * an object containing properties for proper handling of unmapped fields in the UI - */ - unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; - /** - * determines whether to display unmapped fields - * configurable through the switch in the UI - */ - showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - }; } export function DiscoverSidebar({ alwaysShowActionButtons = false, columns, + config, fieldCounts, fieldFilter, hits, indexPatternList, + indexPatterns, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, services, + setAppState, setFieldFilter, - setIndexPattern, + state, trackUiMetric, useNewFieldsApi = false, useFlyout = false, @@ -240,9 +159,13 @@ export function DiscoverSidebar({ })} > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> ); @@ -266,9 +189,13 @@ export function DiscoverSidebar({ > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7ee6cb56d99f2..02ab5abade7fb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -15,15 +15,19 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; -import { FieldFilterState } from './lib/field_filter'; -import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -56,7 +60,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -85,25 +89,25 @@ function getCompProps() { } return { columns: ['extension'], + config: configMock, fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), + setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), - fieldFilter: {} as FieldFilterState, - setFieldFilter: jest.fn(), }; } describe('discover responsive sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + let props: DiscoverSidebarResponsiveProps; + let comp: ReactWrapper; beforeAll(() => { props = getCompProps(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b8e8fd0679baa..b689db1296922 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -11,6 +11,7 @@ import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UiCounterMetricType } from '@kbn/analytics'; +import { IUiSettingsClient } from 'kibana/public'; import { EuiTitle, EuiHideFor, @@ -25,13 +26,14 @@ import { EuiPortal, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes } from '../../../../../data/common'; +import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverSidebarResponsiveProps { /** @@ -42,6 +44,10 @@ export interface DiscoverSidebarResponsiveProps { * the selected columns displayed in the doc table in discover */ columns: string[]; + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * a statistics of the distribution of fields in the given hits */ @@ -54,6 +60,10 @@ export interface DiscoverSidebarResponsiveProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * Has been toggled closed */ @@ -80,9 +90,13 @@ export interface DiscoverSidebarResponsiveProps { */ services: DiscoverServices; /** - * Callback function to select another index pattern + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state */ - setIndexPattern: (id: string) => void; + state: AppState; /** * Metric tracking function * @param metricType @@ -151,9 +165,13 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )} > o.attributes.title)} + indexPatterns={props.indexPatterns} + state={props.state} + setAppState={props.setAppState} + useNewFieldsApi={props.useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index b73f7391bf22a..ee06bcab6528b 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -9,7 +9,7 @@ import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; import { Chart } from '../angular/helpers/point_series'; import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; -import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { AggConfigs } from '../../../../data/common/search/aggs'; import { @@ -23,6 +23,7 @@ import { import { SavedSearch } from '../../saved_searches'; import { AppState, GetStateReturn } from '../angular/discover_state'; import { RequestAdapter } from '../../../../inspector/common'; +import { DiscoverServices } from '../../build_services'; export interface DiscoverProps { /** @@ -59,38 +60,10 @@ export interface DiscoverProps { * Increased when scrolling down */ minimumVisibleRows: number; - /** - * Function to add a column to state - */ - onAddColumn: (column: string) => void; - /** - * Function to add a filter to state - */ - onAddFilter: DocViewFilterFn; - /** - * Function to change the used time interval of the date histogram - */ - onChangeInterval: (interval: string) => void; - /** - * Function to move a given column to a given index, used in legacy table - */ - onMoveColumn: (columns: string, newIdx: number) => void; - /** - * Function to remove a given column from state - */ - onRemoveColumn: (column: string) => void; - /** - * Function to replace columns in state - */ - onSetColumns: (columns: string[]) => void; /** * Function to scroll down the legacy table to the bottom */ onSkipBottomButtonClick: () => void; - /** - * Function to change sorting of the table, triggers a fetch - */ - onSort: (sort: string[][]) => void; opts: { /** * Date histogram aggregation config @@ -108,10 +81,6 @@ export interface DiscoverProps { * Use angular router for navigation */ navigateTo: () => void; - /** - * Functions to get/mutate state - */ - stateContainer: GetStateReturn; /** * Inspect, for analyzing requests and responses */ @@ -128,6 +97,10 @@ export interface DiscoverProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; /** * The number of documents that can be displayed in the table/grid */ @@ -140,6 +113,10 @@ export interface DiscoverProps { * Function to set the header menu */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Functions for retrieving/mutating state + */ + stateContainer: GetStateReturn; /** * Timefield of the currently used index pattern */ @@ -165,18 +142,10 @@ export interface DiscoverProps { * Instance of SearchSource, the high level search API */ searchSource: ISearchSource; - /** - * Function to change the current index pattern - */ - setIndexPattern: (id: string) => void; /** * Current app state of URL */ state: AppState; - /** - * Function to update the time filter - */ - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; /** * Currently selected time range */ @@ -185,10 +154,6 @@ export interface DiscoverProps { * Function to update the actual query */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - /** - * Function to update the actual savedQuery id - */ - updateSavedQueryId: (savedQueryId?: string) => void; /** * An object containing properties for proper handling of unmapped fields in the UI */ diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index b97b6f46600ae..4ade7d1768419 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsService + indexPatternsService: IndexPatternsContract ) { if (!indexPattern.id) return; const field = indexPattern.fields.getByName(fieldName); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 763186fc17c0c..a8ecb384f782b 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -42,6 +42,10 @@ describe('embeddable state transfer', () => { const destinationApp = 'superUltraVisualize'; const originatingApp = 'superUltraTestDashboard'; + const testAppId = 'testApp'; + + const buildKey = (appId: string, key: string) => `${appId}-${key}`; + beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -82,7 +86,9 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -98,7 +104,9 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -117,7 +125,10 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -133,7 +144,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -151,42 +165,92 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, + }); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); + expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + }); + + it('can fetch an incoming editor state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'whoops not me', + }, + [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'otherTestDashboard', + }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + + const fetchedState2 = stateTransfer.getIncomingEditorState('otherApp2'); + expect(fetchedState2).toEqual({ originatingApp: 'otherTestDashboard' }); }); it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toBeUndefined(); }); it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); + it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, + }); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); + + const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2'); + expect(fetchedState2).toEqual({ + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }); + }); + it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); }); it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEmbeddablePackage(true); + stateTransfer.getIncomingEmbeddablePackage(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); @@ -194,10 +258,12 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superCoolFootballDashboard', + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEditorState(true); + stateTransfer.getIncomingEditorState(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index d3b1c1c76aadf..8664a5aae7345 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -50,13 +50,18 @@ export class EmbeddableStateTransfer { public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title; /** - * Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage + * Fetches an {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id * + * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined { + public getIncomingEditorState( + appId: string, + removeAfterFetch?: boolean + ): EmbeddableEditorState | undefined { return this.getIncomingState( isEmbeddableEditorState, + appId, EMBEDDABLE_EDITOR_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined, @@ -64,24 +69,33 @@ export class EmbeddableStateTransfer { ); } - public clearEditorState() { + /** + * Clears the {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id + * + * @param appId - The app to fetch incomingEditorState for + * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. + */ + public clearEditorState(appId: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } /** - * Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage + * Fetches an {@link EmbeddablePackageState | embeddable package} from the sessionStorage for the given AppId * + * @param appId - The app to fetch EmbeddablePackageState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ public getIncomingEmbeddablePackage( + appId: string, removeAfterFetch?: boolean ): EmbeddablePackageState | undefined { return this.getIncomingState( isEmbeddablePackageState, + appId, EMBEDDABLE_PACKAGE_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined, @@ -122,20 +136,27 @@ export class EmbeddableStateTransfer { }); } + private buildKey(appId: string, key: string) { + return `${appId}-${key}`; + } + private getIncomingState( guard: (state: unknown) => state is IncomingStateType, + appId: string, key: string, options?: { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ + this.buildKey(appId, key) + ]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[keyToRemove]; + delete stateReplace[this.buildKey(appId, keyToRemove)]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -150,9 +171,9 @@ export class EmbeddableStateTransfer { const stateObject = options?.appendToExistingState ? { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [key]: options.state, + [this.buildKey(appId, key)]: options.state, } - : { [key]: options?.state }; + : { [this.buildKey(appId, key)]: options?.state }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2f9b43121b45a..3e7014d54958d 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,11 +590,10 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - // (undocumented) - clearEditorState(): void; + clearEditorState(appId: string): void; getAppNameFromId: (appId: string) => string | undefined; - getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; - getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; + getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; + getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; // (undocumented) isTransferInProgress: boolean; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2..93ffaa93cd80e 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -56,7 +56,7 @@ export class InspectorPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); this.views.register(getRequestsViewDescription()); diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 3433d076ee800..ac38f300bd02b 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -7,16 +7,13 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + public setup({ http }: CoreSetup) { + const globalConfig = this.initContext.config.legacy.get(); const router = http.createRouter(); registerRoutes( diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js index 69531013abae4..fd02f83d72823 100644 --- a/src/plugins/maps_legacy/public/leaflet.js +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -12,7 +12,6 @@ if (!window.hasOwnProperty('L')) { window.L.Browser.touch = false; window.L.Browser.pointer = false; - require('leaflet-vega'); require('leaflet.heat/dist/leaflet-heat.js'); require('leaflet-draw/dist/leaflet.draw.css'); require('leaflet-draw/dist/leaflet.draw.js'); diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 00d51da501834..4f35c1c1e5fc1 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -8,7 +8,6 @@ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; import { configSchema, MapsLegacyConfig } from '../config'; import { getUiSettings } from './ui_settings'; @@ -30,7 +29,7 @@ export const config: PluginConfigDescriptor = { }; export interface MapsLegacyPluginSetup { - config$: Observable; + config: MapsLegacyConfig; } export class MapsLegacyPlugin implements Plugin { @@ -43,10 +42,9 @@ export class MapsLegacyPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); - // @ts-ignore - const config$ = this._initializerContext.config.create(); + const pluginConfig = this._initializerContext.config.get(); return { - config$, + config: pluginConfig, }; } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 15efbf38e7b93..6f74198bb56ab 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -31,10 +31,10 @@ export class PresentationUtilPlugin return {}; } - public async start( + public start( coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps - ): Promise { + ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); return { diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index d5d57da400a51..a3a2331cf8f76 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -79,7 +79,7 @@ export class RegionMapPlugin implements Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup) { - const config = await this.initializerContext.config - .create() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup) { + const config = this.initializerContext.config.get(); const collectorSet = new CollectorSet({ logger: this.logger.get('collector-set'), maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); setupRoutes({ diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap new file mode 100644 index 0000000000000..85cf9422630d6 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableVisBasic should init data grid 1`] = ` + + + +`; + +exports[`TableVisBasic should init data grid with title provided - for split mode 1`] = ` + + +

+ My data table +

+
+ +
+`; + +exports[`TableVisBasic should render the toolbar 1`] = ` + + , + "showColumnSelector": false, + "showFullScreenSelector": false, + "showSortSelector": false, + "showStyleSelector": false, + } + } + /> + +`; diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap new file mode 100644 index 0000000000000..b380b85f7f356 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis cell should return a cell component with data in scope 1`] = ` +
+`; diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx new file mode 100644 index 0000000000000..0fb74a41b5df0 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not 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 { shallow } from 'enzyme'; +import { TableVisBasic } from './table_vis_basic'; +import { FormattedColumn, TableVisConfig, TableVisUiState } from '../types'; +import { DatatableColumn } from 'src/plugins/expressions'; +import { createTableVisCell } from './table_vis_cell'; +import { createGridColumns } from './table_vis_columns'; + +jest.mock('./table_vis_columns', () => ({ + createGridColumns: jest.fn(() => []), +})); +jest.mock('./table_vis_cell', () => ({ + createTableVisCell: jest.fn(() => () => {}), +})); + +describe('TableVisBasic', () => { + const props = { + fireEvent: jest.fn(), + table: { + columns: [], + rows: [], + formattedColumns: { + test: { + formattedTotal: 100, + } as FormattedColumn, + }, + }, + visConfig: {} as TableVisConfig, + uiStateProps: { + sort: { + columnIndex: null, + direction: null, + }, + columnsWidth: [], + setColumnsWidth: jest.fn(), + setSort: jest.fn(), + }, + }; + + it('should init data grid', () => { + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should init data grid with title provided - for split mode', () => { + const title = 'My data table'; + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should render the toolbar', () => { + const comp = shallow( + + ); + expect(comp).toMatchSnapshot(); + }); + + it('should sort rows by column and pass the sorted rows for consumers', () => { + const uiStateProps = { + ...props.uiStateProps, + sort: { + columnIndex: 1, + direction: 'desc', + } as TableVisUiState['sort'], + }; + const table = { + columns: [{ id: 'first' }, { id: 'second' }] as DatatableColumn[], + rows: [ + { first: 1, second: 2 }, + { first: 3, second: 4 }, + { first: 5, second: 6 }, + ], + formattedColumns: {}, + }; + const sortedRows = [ + { first: 5, second: 6 }, + { first: 3, second: 4 }, + { first: 1, second: 2 }, + ]; + const comp = shallow( + + ); + expect(createTableVisCell).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(createGridColumns).toHaveBeenCalledWith( + table.columns, + sortedRows, + table.formattedColumns, + uiStateProps.columnsWidth, + props.fireEvent + ); + + const { onSort } = comp.find('EuiDataGrid').prop('sorting'); + // sort the first column + onSort([{ id: 'first', direction: 'asc' }]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 0, direction: 'asc' }); + // sort the second column - should erase the first column sorting since there is only one level sorting available + onSort([ + { id: 'first', direction: 'asc' }, + { id: 'second', direction: 'desc' }, + ]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 1, direction: 'desc' }); + }); + + it('should pass renderFooterCellValue for the total row', () => { + const comp = shallow( + + ); + const renderFooterCellValue: (props: any) => void = comp + .find('EuiDataGrid') + .prop('renderFooterCellValue'); + expect(renderFooterCellValue).toEqual(expect.any(Function)); + expect(renderFooterCellValue({ columnId: 'test' })).toEqual(100); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 0000000000000..322ceacbe002e --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.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 and the Server Side Public License, v 1; you may not 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 { shallow } from 'enzyme'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { createTableVisCell } from './table_vis_cell'; +import { FormattedColumns } from '../types'; + +describe('table vis cell', () => { + it('should return a cell component with data in scope', () => { + const rows = [{ first: 1, second: 2 }]; + const formattedColumns = ({ + second: { + formatter: { + convert: jest.fn(), + }, + }, + } as unknown) as FormattedColumns; + const Cell = createTableVisCell(rows, formattedColumns); + const cellProps = { + rowIndex: 0, + columnId: 'second', + } as EuiDataGridCellValueElementProps; + + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + expect(formattedColumns.second.formatter.convert).toHaveBeenLastCalledWith(2, 'html'); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx new file mode 100644 index 0000000000000..3d169531f5757 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../utils', () => ({ + useUiState: jest.fn(() => 'uiState'), +})); + +import React from 'react'; +import { shallow } from 'enzyme'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { coreMock } from '../../../../core/public/mocks'; +import { TableVisConfig, TableVisData } from '../types'; +import TableVisualizationComponent from './table_visualization'; +import { useUiState } from '../utils'; + +describe('TableVisualizationComponent', () => { + const coreStartMock = coreMock.createStart(); + const handlers = ({ + done: jest.fn(), + uiState: 'uiState', + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visData: TableVisData = { + table: { + columns: [], + rows: [], + formattedColumns: {}, + }, + tables: [], + }; + const visConfig = ({} as unknown) as TableVisConfig; + + it('should render the basic table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); + expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); + }); + + it('should render split table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeTruthy(); + expect(comp.find('.tbvChart__split').exists()).toBeFalsy(); + expect(comp.find('[data-test-subj="tbvChart"]').children().prop('tables')).toEqual([]); + }); +}); diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 4792ceefde536..0a9d477c26691 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; @@ -34,8 +34,7 @@ export interface TablePluginStartDependencies { /** @internal */ export class TableVisPlugin - implements - Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { + implements AsyncPlugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts new file mode 100644 index 0000000000000..0280637acc099 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => 'formatter'), + })), +})); + +import { FieldFormat } from 'src/plugins/data/public'; +import { TableContext } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; + +describe('', () => { + const table: TableContext = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + formattedColumns: { + 'col-0-1': { + sumTotal: 7, + title: 'Count', + filterable: false, + formatter: {} as FieldFormat, + }, + }, + }; + + it('should dnot add percentage column if it was not found', () => { + const output = addPercentageColumn(table, 'Extra'); + expect(output).toBe(table); + }); + + it('should add a brand new percentage column into table based on data', () => { + const output = addPercentageColumn(table, 'Count'); + const expectedColumns = [ + table.columns[0], + { + id: 'col-0-1-percents', + meta: { + params: { + id: 'percent', + }, + type: 'number', + }, + name: 'Count percentages', + }, + table.columns[1], + table.columns[2], + ]; + const expectedRows = [ + { ...table.rows[0], 'col-0-1-percents': 0.14285714285714285 }, + { ...table.rows[1], 'col-0-1-percents': 0.8571428571428571 }, + ]; + expect(output).toEqual({ + columns: expectedColumns, + rows: expectedRows, + formattedColumns: { + ...table.formattedColumns, + 'col-0-1-percents': { + filterable: false, + formatter: 'formatter', + title: 'Count percentages', + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts new file mode 100644 index 0000000000000..0a9c7320d4359 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockDeserialize = jest.fn(() => ({})); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { AggTypes } from '../../common'; +import { TableVisConfig } from '../types'; +import { createFormattedTable } from './create_formatted_table'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [ + { + accessor: 1, + aggType: 'terms', + format: { id: 'string' }, + label: 'category_keyword: Descending', + params: {}, + }, + ], + metrics: [ + { accessor: 0, aggType: 'count', format: { id: 'number' }, label: 'Count', params: {} }, + ], + }, +}; + +describe('createFormattedTable', () => { + const table: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + + it('should create formatted columns from data response and flter out non-dimension columns', () => { + const output = createFormattedTable(table, visConfig); + + // column to split is filtered out of real data representing + expect(output.columns).toEqual([table.columns[0], table.columns[1]]); + expect(output.rows).toEqual(table.rows); + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: {}, + title: 'Count', + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total sum to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, visConfig); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 7, + formattedTotal: 7, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total average to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.AVG }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 3.5, + formattedTotal: 3.5, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find min value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MIN }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 1, + formattedTotal: 1, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find max value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MAX }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 6, + formattedTotal: 6, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add rows count as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.COUNT }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 2, + formattedTotal: 2, + }, + 'col-1-5': { + filterable: true, + formattedTotal: 2, + formatter: {}, + sumTotal: "0Women's ClothingWomen's Clothing", + title: 'category.keyword: Descending', + total: 2, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts new file mode 100644 index 0000000000000..8adc535e802f0 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockConverter = jest.fn((name) => `By ${name}`); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => ({ + convert: mockConverter, + })), + })), +})); + +jest.mock('./create_formatted_table', () => ({ + createFormattedTable: jest.fn((data) => ({ + ...data, + formattedColumns: {}, + })), +})); + +jest.mock('./add_percentage_column', () => ({ + addPercentageColumn: jest.fn((data, column) => ({ + ...data, + percentage: `${column} with percentage`, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { AggTypes } from '../../common'; +import { TableGroup, TableVisConfig } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; +import { createFormattedTable } from './create_formatted_table'; +import { tableVisResponseHandler } from './table_vis_response_handler'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.AVG, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [], + metrics: [], + }, +}; + +describe('tableVisResponseHandler', () => { + describe('basic table', () => { + const input: Datatable = { + columns: [], + rows: [], + type: 'datatable', + }; + + it('should create formatted table for basic usage', () => { + const output = tableVisResponseHandler(input, visConfig); + + expect(output.direction).toBeUndefined(); + expect(output.tables.length).toEqual(0); + expect(addPercentageColumn).not.toHaveBeenCalled(); + expect(createFormattedTable).toHaveBeenCalledWith(input, visConfig); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + }); + }); + + it('should add a percentage column if it is set', () => { + const output = tableVisResponseHandler(input, { ...visConfig, percentageCol: 'Count' }); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + percentage: 'Count with percentage', + }); + }); + }); + + describe('split table', () => { + const input: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-2': 'Men' }, + { 'col-0-1': 3, 'col-1-2': 'Women' }, + { 'col-0-1': 6, 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + const split: SchemaConfig[] = [ + { + accessor: 1, + label: 'Split', + format: {}, + params: {}, + aggType: 'terms', + }, + ]; + const expectedOutput: TableGroup[] = [ + { + title: 'By Men: Gender', + table: { + columns: input.columns, + rows: [input.rows[0], input.rows[2]], + formattedColumns: {}, + }, + }, + { + title: 'By Women: Gender', + table: { + columns: input.columns, + rows: [input.rows[1]], + formattedColumns: {}, + }, + }, + ]; + + it('should split data by row', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitRow: split }, + }); + + expect(output.direction).toEqual('row'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should split data by column', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should add percentage columns to each table', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + percentageCol: 'Count', + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual([ + { + ...expectedOutput[0], + table: { ...expectedOutput[0].table, percentage: 'Count with percentage' }, + }, + { + ...expectedOutput[1], + table: { ...expectedOutput[1].table, percentage: 'Count with percentage' }, + }, + ]); + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts index e0919671135ea..69521c20cddfe 100644 --- a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts @@ -27,7 +27,6 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); const splitColumn = input.columns[splitColumnIndex]; - const columns = input.columns.filter((c, idx) => idx !== splitColumnIndex); const splitMap: { [key: string]: number } = {}; let splitIndex = 0; @@ -39,7 +38,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, table: { - columns, + columns: input.columns, rows: [], formattedColumns: {}, }, @@ -53,7 +52,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon }); tables.forEach((tg) => { - tg.table = createFormattedTable({ ...tg.table, columns: input.columns }, visConfig); + tg.table = createFormattedTable(tg.table, visConfig); if (visConfig.percentageCol) { tg.table = addPercentageColumn(tg.table, visConfig.percentageCol); diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts new file mode 100644 index 0000000000000..3d0b58aa6c8a3 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes } from '../../../common'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + }; + + it('should set up pagination on init', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should skip setting the pagination if perPage is not set', () => { + const { result } = renderHook(() => usePagination({ ...visParams, perPage: '' }, 15)); + + expect(result.current).toBeUndefined(); + }); + + it('should change the page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangePage(1); + }); + + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change items per page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangeItemsPerPage(20); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 20, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change the page when props were changed', () => { + const { result, rerender } = renderHook( + (props) => usePagination(props.visParams, props.rowCount), + { + initialProps: { + visParams, + rowCount: 15, + }, + } + ); + const updatedParams = { ...visParams, perPage: 5 }; + + // change items per page count + rerender({ visParams: updatedParams, rowCount: 15 }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + act(() => { + // change the page to the last one - 3 + result.current?.onChangePage(3); + }); + + expect(result.current).toEqual({ + pageIndex: 3, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + // decrease the rows count + rerender({ visParams: updatedParams, rowCount: 10 }); + + // should switch to the last available page + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts new file mode 100644 index 0000000000000..be1f9d3a10cf7 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import type { PersistedState } from 'src/plugins/visualizations/public'; +import { TableVisUiState } from '../../types'; +import { useUiState } from './use_ui_state'; + +describe('useUiState', () => { + let uiState: PersistedState; + + beforeEach(() => { + uiState = { + get: jest.fn(), + on: jest.fn(), + off: jest.fn(), + set: jest.fn(), + } as any; + }); + + it("should init default columnsWidth & sort if uiState doesn't have it set", () => { + const { result } = renderHook(() => useUiState(uiState)); + + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: null, + direction: null, + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + }); + + it('should subscribe on uiState changes and update local state', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => useUiState(uiState)); + + expect(uiState.on).toHaveBeenCalledWith('change', expect.any(Function)); + // @ts-expect-error + const updateOnChange = uiState.on.mock.calls[0][1]; + + uiState.getChanges = jest.fn(() => ({ + vis: { + params: { + sort: { + columnIndex: 1, + direction: 'asc', + }, + colWidth: [], + }, + }, + })); + + act(() => { + updateOnChange(); + }); + + await waitForNextUpdate(); + + // should update local state with new values + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + act(() => { + updateOnChange(); + }); + + // should skip setting the state again if it is equal + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + unmount(); + + expect(uiState.off).toHaveBeenCalledWith('change', updateOnChange); + }); + + describe('updating uiState through callbacks', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('should update the uiState with new sort', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const newSort: TableVisUiState['sort'] = { + columnIndex: 5, + direction: 'desc', + }; + + act(() => { + result.current.setSort(newSort); + }); + + expect(result.current.sort).toEqual(newSort); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenCalledWith('vis.params.sort', newSort); + }); + + it('should update the uiState with new columns width', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const col1 = { colIndex: 0, width: 300 }; + const col2 = { colIndex: 1, width: 100 }; + + // set width of a column + act(() => { + result.current.setColumnsWidth(col1); + }); + + expect(result.current.columnsWidth).toEqual([col1]); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1]); + + // set width of another column + act(() => { + result.current.setColumnsWidth(col2); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(2); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1, col2]); + + const updatedCol1 = { colIndex: 0, width: 200 }; + // update width of existing column + act(() => { + result.current.setColumnsWidth(updatedCol1); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(3); + expect(uiState.set).toHaveBeenCalledWith('vis.params.colWidth', [updatedCol1, col2]); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + }); +}); diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index e31aae7fcdda7..1dcb7263c4818 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -8,7 +8,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { Plugin } from './plugin'; +import { TimelionPlugin } from './plugin'; export { PluginSetupContract } from './plugin'; @@ -25,4 +25,4 @@ export const config: PluginConfigDescriptor = { ], }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new TimelionPlugin(initializerContext); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 2bb8f7214f904..c1800a09ba35c 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -7,13 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { deepFreeze } from '@kbn/std'; import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; @@ -39,16 +38,12 @@ export interface TimelionPluginStartDeps { /** * Represents Timelion Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class TimelionPlugin + implements Plugin, void, TimelionPluginStartDeps> { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup( - core: CoreSetup - ): Promise> { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup): RecursiveReadonly { + const config = this.initializerContext.config.get>(); const configManager = new ConfigManager(this.initializerContext.config); diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 59ae89300705e..6900630ffa971 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -43,14 +43,14 @@ export interface MetricsPluginStartDependencies { } /** @internal */ -export class MetricsPlugin implements Plugin, void> { +export class MetricsPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8d6a8227203d2..042ffac583e98 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -10,6 +10,8 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; +import { Assign } from '@kbn/utility-types'; +import { Spec } from 'vega'; import { EsQueryParser } from './es_query_parser'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; @@ -93,21 +95,24 @@ export interface KibanaConfig { renderer: Renderer; } -export interface VegaSpec { - [index: string]: any; - $schema: string; - data?: Data; - encoding?: Encoding; - mark?: string; - title?: string; - autosize?: AutoSize; - projections?: Projection[]; - width?: number | 'container'; - height?: number | 'container'; - padding?: number | Padding; - _hostConfig?: KibanaConfig; - config: VegaSpecConfig; -} +export type VegaSpec = Assign< + Spec, + { + [index: string]: any; + $schema: string; + data?: Data; + encoding?: Encoding; + mark?: string; + title?: string; + autosize?: AutoSize; + projections?: Projection[]; + width?: number | 'container'; + height?: number | 'container'; + padding?: number | Padding; + _hostConfig?: KibanaConfig; + config: VegaSpecConfig; + } +>; export enum CONSTANTS { TIMEFILTER = '%timefilter%', diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index eeacec0834ea6..1948792d55a83 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -13,11 +13,6 @@ import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; jest.mock('../services'); -jest.mock('../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - describe(`VegaParser.parseAsync`, () => { test(`should throw an error in case of $spec is not defined`, async () => { const vp = new VegaParser('{}'); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 667350b693a54..e97418581a42f 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -13,8 +13,9 @@ import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { vega, vegaLite } from '../lib/vega'; + +import { logger, Warn, version as vegaVersion } from 'vega'; +import { compile, TopLevelSpec, version as vegaLiteVersion } from 'vega-lite'; import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; @@ -235,9 +236,9 @@ The URL is an identifier only. Kibana and your browser will never access this UR */ private _compileVegaLite() { this.vlspec = this.spec; - const logger = vega.logger(vega.Warn); // note: eslint has a false positive here - logger.warn = this._onWarning.bind(this); - this.spec = vegaLite.compile(this.vlspec, logger).spec; + const vegaLogger = logger(Warn); // note: eslint has a false positive here + vegaLogger.warn = this._onWarning.bind(this); + this.spec = compile(this.vlspec as TopLevelSpec, { logger: vegaLogger }).spec; // When using VL with the type=map and user did not provid their own projection settings, // remove the default projection that was generated by VegaLite compiler. @@ -534,7 +535,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR private parseSchema(spec: VegaSpec) { const schema = schemaParser(spec.$schema); const isVegaLite = schema.library === 'vega-lite'; - const libVersion = isVegaLite ? vegaLite.version : vega.version; + const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; if (versionCompare(schema.version, libVersion) > 0) { this._onWarning( diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea99..7cc70f31589c7 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -54,14 +54,14 @@ export interface VegaPluginStartDependencies { } /** @internal */ -export class VegaPlugin implements Plugin, void> { +export class VegaPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index fb36a0097c970..76479cbcdf1ec 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -16,7 +16,7 @@ import { VegaInspectorAdapters } from './vega_inspector/index'; import { KibanaContext, TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; -type Input = KibanaContext | null; +type Input = KibanaContext | { type: 'null' }; type Output = Promise>; interface Arguments { diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 54d4cf16f0cde..902f79d03e680 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -19,7 +19,6 @@ import { toExpressionAst } from './to_ast'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; -import type { VegaSpec } from './data_model/types'; import type { VisParams } from './vega_fn'; export const createVegaTypeDefinition = (): VisTypeDefinition => { @@ -58,7 +57,7 @@ export const createVegaTypeDefinition = (): VisTypeDefinition => { try { const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); - return extractIndexPatternsFromSpec(spec as VegaSpec); + return extractIndexPatternsFromSpec(spec); } catch (e) { // spec is invalid } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 2ef687594ce06..d9b1b536a6d17 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -9,7 +9,8 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { vega, vegaLite } from '../lib/vega'; +import { scheme, loader, logger, Warn, version as vegaVersion, expressionFunction } from 'vega'; +import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,7 +20,7 @@ import { esFilters } from '../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; -vega.scheme('elastic', euiPaletteColorBlind()); +scheme('elastic', euiPaletteColorBlind()); // Vega's extension functions are global. When called, // we forward execution to the instance-specific handler @@ -32,8 +33,8 @@ const vegaFunctions = { }; for (const funcName of Object.keys(vegaFunctions)) { - if (!vega.expressionFunction(funcName)) { - vega.expressionFunction(funcName, function handlerFwd(...args) { + if (!expressionFunction(funcName)) { + expressionFunction(funcName, function handlerFwd(...args) { const view = this.context.dataflow; view.runAfter(() => view._kibanaView.vegaFunctionsHandler(funcName, ...args)); }); @@ -164,9 +165,9 @@ export class VegaBaseView { }; // Override URL sanitizer to prevent external data loading (if disabled) - const loader = vega.loader(); - const originalSanitize = loader.sanitize.bind(loader); - loader.sanitize = (uri, options) => { + const vegaLoader = loader(); + const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); + vegaLoader.sanitize = (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -185,14 +186,14 @@ export class VegaBaseView { } return originalSanitize(uri, options); }; - config.loader = loader; + config.loader = vegaLoader; - const logger = vega.logger(vega.Warn); + const vegaLogger = logger(Warn); - logger.warn = this.onWarn.bind(this); - logger.error = this.onError.bind(this); + vegaLogger.warn = this.onWarn.bind(this); + vegaLogger.error = this.onError.bind(this); - config.logger = logger; + config.logger = vegaLogger; return config; } @@ -430,8 +431,8 @@ export class VegaBaseView { } const debugObj = {}; window.VEGA_DEBUG = debugObj; - window.VEGA_DEBUG.VEGA_VERSION = vega.version; - window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLite.version; + window.VEGA_DEBUG.VEGA_VERSION = vegaVersion; + window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLiteVersion; window.VEGA_DEBUG.view = view; window.VEGA_DEBUG.vega_spec = spec; window.VEGA_DEBUG.vegalite_spec = vlspec; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts index f200d27e1b967..3dc245f196774 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Style } from 'mapbox-gl'; import { TMS_IN_YML_ID } from '../../../../maps_legacy/public'; export const vegaLayerId = 'vega'; @@ -16,7 +17,7 @@ export const defaultMapConfig = { tileSize: 256, }; -export const defaultMabBoxStyle = { +export const defaultMabBoxStyle: Style = { /** * according to the MapBox documentation that value should be '8' * @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts index 963c2bd03f415..da4c14c77bc98 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts @@ -7,6 +7,7 @@ */ import { initVegaLayer } from './vega_layer'; +import type { View } from 'vega'; type InitVegaLayerParams = Parameters[0]; @@ -32,9 +33,9 @@ describe('vega_map_view/tms_raster_layer', () => { addLayer: jest.fn(), } as unknown) as MapType; context = { - vegaView: { + vegaView: ({ initialize: jest.fn(), - }, + } as unknown) as View, updateVegaView: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts index 884e948e2aea3..a3efba804b454 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts @@ -7,14 +7,12 @@ */ import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { View } from 'vega'; import type { LayerParameters } from './types'; -// @ts-ignore -import { vega } from '../../lib/vega'; - export interface VegaLayerContext { - vegaView: vega.View; - updateVegaView: (map: Map, view: vega.View) => void; + vegaView: View; + updateVegaView: (map: Map, view: View) => void; } export function initVegaLayer({ diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts index 29c8d33cf3967..2085e250045f6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts @@ -7,13 +7,12 @@ */ // @ts-expect-error -// eslint-disable-next-line import/no-extraneous-dependencies import Vsi from 'vega-spec-injector'; -import { VegaSpec } from '../../../data_model/types'; +import { Spec } from 'vega'; import { defaultProjection } from '../constants'; -export const injectMapPropsIntoSpec = (spec: VegaSpec) => { +export const injectMapPropsIntoSpec = (spec: Spec) => { const vsi = new Vsi(); vsi.overrideField(spec, 'autosize', 'none'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index b59e1c65ab3f8..21c18e15c242c 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,11 +28,8 @@ import { setMapServiceSettings, setUISettings, } from '../../services'; - -jest.mock('../../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); +import { initVegaLayer, initTmsRasterLayer } from './layers'; +import { Map, NavigationControl, Style } from 'mapbox-gl'; jest.mock('mapbox-gl', () => ({ Map: jest.fn().mockImplementation(() => ({ @@ -55,9 +52,6 @@ jest.mock('./layers', () => ({ initTmsRasterLayer: jest.fn(), })); -import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl } from 'mapbox-gl'; - describe('vega_map_view/view', () => { describe('VegaMapView', () => { const coreStart = coreMock.createStart(); @@ -76,7 +70,7 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: () => ({ + getVectorStyleSheet: (): Style => ({ version: 8, sources: {}, layers: [], diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 1cdc3af733589..4c155d6b5ea88 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import { View, parse } from 'vega'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -24,12 +25,9 @@ import { import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; -// @ts-expect-error -import { vega } from '../../lib/vega'; - import './vega_map_view.scss'; -async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) { +async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); let shouldRender = false; @@ -77,7 +75,7 @@ export class VegaMapView extends VegaBaseView { }; } - private async initMapContainer(vegaView: vega.View) { + private async initMapContainer(vegaView: View) { let style: Style = defaultMabBoxStyle; let customAttribution: MapboxOptions['customAttribution'] = []; const zoomSettings = { @@ -139,7 +137,7 @@ export class VegaMapView extends VegaBaseView { } } - private initLayers(mapBoxInstance: Map, vegaView: vega.View) { + private initLayers(mapBoxInstance: Map, vegaView: View) { const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { @@ -168,8 +166,8 @@ export class VegaMapView extends VegaBaseView { } protected async _initViewCustomizations() { - const vegaView = new vega.View( - vega.parse(injectMapPropsIntoSpec(this._parser.spec)), + const vegaView = new View( + parse(injectMapPropsIntoSpec(this._parser.spec)), this._vegaViewConfig ); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 5d5f3ed3d3733..5b1e49a73343b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { vega } from '../lib/vega'; +import { View, parse } from 'vega'; import { VegaBaseView } from './vega_base_view'; export class VegaView extends VegaBaseView { @@ -14,7 +14,7 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); + const view = new View(parse(this._parser.spec), this._vegaViewConfig); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a55d5c4423f0e..776f8898b3e3a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -26,11 +26,6 @@ jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), })); -jest.mock('./lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - // FLAKY: https://github.com/elastic/kibana/issues/71713 describe('VegaVisualizations', () => { let domNode; diff --git a/src/plugins/vis_type_vega/server/types.ts b/src/plugins/vis_type_vega/server/types.ts index f1e97416d7665..affd93dedb8ca 100644 --- a/src/plugins/vis_type_vega/server/types.ts +++ b/src/plugins/vis_type_vega/server/types.ts @@ -7,10 +7,11 @@ */ import { Observable } from 'rxjs'; +import { SharedGlobalConfig } from 'kibana/server'; import { HomeServerPluginSetup } from '../../home/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; -export type ConfigObservable = Observable<{ kibana: { index: string } }>; +export type ConfigObservable = Observable; export interface VegaSavedObjectAttributes { title: string; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index b3abc46070159..9db1b7657f444 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -12,11 +12,12 @@ import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/ser import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; +import { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { const mockIndex = 'mock_index'; const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; - const mockConfig = of({ kibana: { index: mockIndex } }); + const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; it('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index c013056ba4566..d03ee6eae790e 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "strictNullChecks": false }, "include": [ "server/**/*", diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index b266a681f8031..9d329c92bede0 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -46,7 +46,7 @@ export class VisTypeVislibPlugin Plugin { constructor(public initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 5be971a085d3c..75a2f4fb6895c 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -59,7 +59,7 @@ export class VisTypeXyPlugin VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies > { - public async setup( + public setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 6ca6efaa89797..fa0e0bd5f48f0 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -34,7 +34,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 7465e7eaa9044..c6333e978183f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -22,6 +22,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -54,7 +55,8 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; + const { originatingApp: value } = + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c772554344cb2..bc766d63db5a7 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -65,7 +65,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(); + stateTransferService.clearEditorState(VisualizeConstants.APP_ID); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 9ea42e8b56559..e8c3289d4ce41 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -142,7 +142,7 @@ export const getTopNavConfig = ( if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(VisualizeConstants.APP_ID); } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 7dbf5be77b74d..6e901882a9365 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -16,4 +16,5 @@ export const VisualizeConstants = { CREATE_PATH: '/create', EDIT_PATH: '/edit', EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca396..4eb2d6fd2a731 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -84,7 +84,7 @@ export class VisualizePlugin constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, { home, urlForwarding, data }: VisualizePluginSetupDependencies ) { @@ -132,7 +132,7 @@ export class VisualizePlugin setUISettings(core.uiSettings); core.application.register({ - id: 'visualize', + id: VisualizeConstants.APP_ID, title: 'Visualize', order: 8000, euiIconType: 'logoKibana', @@ -147,7 +147,9 @@ export class VisualizePlugin // allows the urlTracker to only save URLs that are not linked to an originatingApp this.isLinkedToOriginatingApp = () => { return Boolean( - pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + pluginsStart.embeddable + .getStateTransfer() + .getIncomingEditorState(VisualizeConstants.APP_ID)?.originatingApp ); }; diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts index a6ecd491f169f..8481e2bf334aa 100644 --- a/test/accessibility/apps/kibana_overview.ts +++ b/test/accessibility/apps/kibana_overview.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('kibanaOverview'); }); @@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { useActualUrl: true, }); await PageObjects.home.removeSampleDataSet('flights'); - await esArchiver.unload('empty_kibana'); }); it('Getting started view', async () => { diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 64ef11167b333..b889b59fdaf32 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -11,11 +11,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7; describe('sample data apis', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); describe('list', () => { it('should return list of sample data sets with installed status', async () => { const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200); diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index e84052e58dac4..87c5aa535ccd9 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -17,6 +17,55 @@ export default function ({ getService }) { expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); }; + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + }, + ]; + describe('fields_for_wildcard_route response', () => { before(() => esArchiver.load('index_patterns/basic_index')); after(() => esArchiver.unload('index_patterns/basic_index')); @@ -26,54 +75,7 @@ export default function ({ getService }) { .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index' }) .expect(200, { - fields: [ - { - type: 'boolean', - esTypes: ['boolean'], - searchable: true, - aggregatable: true, - name: 'bar', - readFromDocValues: true, - }, - { - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - name: 'baz', - readFromDocValues: false, - }, - { - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - name: 'baz.keyword', - readFromDocValues: true, - subType: { multi: { parent: 'baz' } }, - }, - { - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - name: 'foo', - readFromDocValues: true, - }, - { - aggregatable: true, - esTypes: ['keyword'], - name: 'nestedField.child', - readFromDocValues: true, - searchable: true, - subType: { - nested: { - path: 'nestedField', - }, - }, - type: 'string', - }, - ], + fields: testFields, }) .then(ensureFieldsAreSorted); }); @@ -162,11 +164,19 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); - it('returns 404 when the pattern does not exist', async () => { + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ pattern: 'bad_index,basic_index' }) + .expect(200, { + fields: testFields, + }); + }); + it('returns 404 when no patterns exist', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ - pattern: '[non-existing-pattern]its-invalid-*', + pattern: 'bad_index', }) .expect(404); }); diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 6239b930434af..57b7ff0935f58 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -97,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 with individual responses', async () => + it('should return 200 with errors', async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); await supertest .post('/api/saved_objects/_bulk_create') .send(BULK_REQUESTS) @@ -109,38 +110,27 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ saved_objects: [ { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - updated_at: resp.body.saved_objects[0].updated_at, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'An existing visualization', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - visualization: resp.body.saved_objects[0].migrationVersion.visualization, + id: BULK_REQUESTS[0].id, + type: BULK_REQUESTS[0].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version }, { - type: 'dashboard', - id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + id: BULK_REQUESTS[1].id, + type: BULK_REQUESTS[1].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, }, ], }); - })); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index e9514d7d55457..77f84dee25ded 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with individual responses', async () => diff --git a/test/api_integration/apis/saved_objects/bulk_update.ts b/test/api_integration/apis/saved_objects/bulk_update.ts index d9e3c27869591..a5f5262196346 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.ts +++ b/test/api_integration/apis/saved_objects/bulk_update.ts @@ -235,10 +235,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => { + it('should return 200 with errors', async () => { const response = await supertest .put(`/api/saved_objects/_bulk_update`) .send([ @@ -267,9 +267,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); @@ -277,9 +277,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', type: 'dashboard', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); }); diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index 355e5df1f1895..de31b621a6480 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -83,10 +83,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 and create kibana index', async () => { + it('should return 500 and not auto-create saved objects index', async () => { await supertest .post(`/api/saved_objects/visualization`) .send({ @@ -94,50 +94,16 @@ export default function ({ getService }: FtrProviderContext) { title: 'My favorite vis', }, }) - .expect(200) + .expect(500) .then((resp) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - coreMigrationVersion: KIBANA_VERSION, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - namespaces: ['default'], + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, }); - expect(resp.body.migrationVersion).to.be.ok(); }); - expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true); - }); - - it('result should have the latest coreMigrationVersion', async () => { - await supertest - .post(`/api/saved_objects/visualization`) - .send({ - attributes: { - title: 'My favorite vis', - }, - coreMigrationVersion: '1.2.3', - }) - .expect(200) - .then((resp) => { - expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); - }); + expect((await es.indices.exists({ index: '.kibana' })).body).to.be(false); }); }); }); diff --git a/test/api_integration/apis/saved_objects/delete.ts b/test/api_integration/apis/saved_objects/delete.ts index 5247bc74131d4..0dfece825d3a1 100644 --- a/test/api_integration/apis/saved_objects/delete.ts +++ b/test/api_integration/apis/saved_objects/delete.ts @@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('returns generic 404 when kibana index is missing', async () => diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 32a72f374cbe1..5206d51054745 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -534,7 +534,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return empty response', async () => { diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index be31e0faf1e46..66c2a083c79e5 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) { score: 0, type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzYsMV0=', + version: 'WzIyLDJd', }, ], }); @@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: KIBANA_VERSION, updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzIsMV0=', + version: 'WzE4LDJd', }, ], }); @@ -426,11 +426,11 @@ export default function ({ getService }: FtrProviderContext) { })); }); - describe.skip('without kibana index', () => { + describe('without kibana index', () => { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index f912a2efcf0d9..84ab6e36956d5 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return basic 404 without mentioning index', async () => diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 4fcce29905beb..b203a2c7b7071 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); describe('resolve_import_errors', () => { // mock success results including metadata @@ -34,7 +35,11 @@ export default function ({ getService }: FtrProviderContext) { describe('without kibana index', () => { // Cleanup data that got created in import - after(() => esArchiver.unload('saved_objects/basic')); + before( + async () => + // just in case the kibana server has recreated it + await esDeleteAllIndices('.kibana*') + ); it('should return 200 and import nothing when empty parameters are passed in', async () => { await supertest @@ -51,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return 200 and import everything when overwrite parameters contains all objects', async () => { + it('should return 200 with internal server errors', async () => { await supertest .post('/api/saved_objects/_resolve_import_errors') .field( @@ -78,12 +83,42 @@ export default function ({ getService }: FtrProviderContext) { .expect(200) .then((resp) => { expect(resp.body).to.eql({ - success: true, - successCount: 3, - successResults: [ - { ...indexPattern, overwrite: true }, - { ...visualization, overwrite: true }, - { ...dashboard, overwrite: true }, + successCount: 0, + success: false, + errors: [ + { + ...indexPattern, + ...{ title: indexPattern.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...visualization, + ...{ title: visualization.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...dashboard, + ...{ title: dashboard.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, ], warnings: [], }); diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts index ce14e9cea7b13..631046a0564a3 100644 --- a/test/api_integration/apis/saved_objects/update.ts +++ b/test/api_integration/apis/saved_objects/update.ts @@ -121,10 +121,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => + it('should return 500', async () => await supertest .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ @@ -132,13 +132,12 @@ export default function ({ getService }: FtrProviderContext) { title: 'My second favorite vis', }, }) - .expect(404) + .expect(500) .then((resp) => { expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: - 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 87de59a94fd24..6ab2352ebb05f 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -42,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts index 69c85428d0624..4dfd06a61eecf 100644 --- a/test/api_integration/apis/saved_objects_management/get.ts +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 404 for object that no longer exists', async () => diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index 2b61ed7586384..bc092dd3889bb 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -17,6 +17,7 @@ export default function ({ getService }: FtrProviderContext) { describe('search', () => { before(async () => { + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts index 2e42fbfc6ac60..7e0564ac44a43 100644 --- a/test/api_integration/apis/telemetry/opt_in.ts +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -14,10 +14,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + describe('/api/telemetry/v2/optIn API', () => { let defaultAttributes: TelemetrySavedObjectAttributes; let kibanaVersion: any; before(async () => { + await esArchiver.emptyKibanaIndex(); const kibanaVersionAccessor = kibanaServer.version; kibanaVersion = await kibanaVersionAccessor.get(); defaultAttributes = diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 23a0d3fb2cd3c..b424cab9ff45b 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -177,6 +177,7 @@ export default function ({ getService }: FtrProviderContext) { describe('basic behaviour', () => { let savedObjectIds: string[] = []; before('create application usage entries', async () => { + await esArchiver.emptyKibanaIndex(); savedObjectIds = await Promise.all([ createSavedObject(), createSavedObject('appView1'), diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index c2286f8ea3dce..c287e73e3ace9 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createUiCounterEvent = (eventName: string, type: UiCounterMetricType, count = 1) => ({ @@ -24,6 +25,9 @@ export default function ({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/85086 describe.skip('UI Counters API', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); const dayDate = moment().format('DDMMYYYY'); it('stores ui counter events in savedObjects', async () => { diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index 1e80487da551a..99007376e1ea4 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createStatsMetric = ( @@ -34,6 +35,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('ui_metric savedObject data', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + it('increments the count field in the document defined by the {app}/{action_type} path', async () => { const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); diff --git a/test/api_integration/config.js b/test/api_integration/config.js index d688c31dc47e7..bd8f10606a45a 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -19,7 +19,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'API Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/common/config.js b/test/common/config.js index 451324d46f62d..46cd07b2ec370 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -21,9 +21,7 @@ export default function () { servers, esTestCluster: { - license: 'oss', - from: 'snapshot', - serverArgs: [], + serverArgs: ['xpack.security.enabled=false'], }, kbnTestServer: { @@ -61,8 +59,6 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), - // Disable v2 migrations in tests for now - '--migrations.enableV2=false', ], }, services, diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index a19118bb3065a..510124ce3d1b7 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -35,17 +35,7 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { * Useful for functional testing in cloud environment */ async isOss() { - const baseUrl = this.getEsHostPort(); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - const response = await fetch(baseUrl + '/_xpack', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - return response.status !== 200; + return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; }, async isCloud(): Promise { diff --git a/test/common/services/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js index c2d01eef267bc..9a06dd7b74969 100644 --- a/test/common/services/kibana_server/extend_es_archiver.js +++ b/test/common/services/kibana_server/extend_es_archiver.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload']; +const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload', 'emptyKibanaIndex']; const KIBANA_INDEX = '.kibana'; export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) { @@ -25,7 +25,7 @@ export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) const statsKeys = Object.keys(stats); const kibanaKeys = statsKeys.filter( // this also matches stats keys like '.kibana_1' and '.kibana_2,.kibana_1' - (key) => key.includes(KIBANA_INDEX) && (stats[key].created || stats[key].deleted) + (key) => key.includes(KIBANA_INDEX) && stats[key].created ); // if the kibana index was created by the esArchiver then update the uiSettings diff --git a/test/examples/config.js b/test/examples/config.js index fd1ad671cf4bf..0ba7af0bfceb7 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 6aeb1e2a624ad..05933ebf1ea2a 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -85,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.dismissTutorial(); expect(await PageObjects.console.hasAutocompleter()).to.be(false); await PageObjects.console.promptAutocomplete(); - retry.waitFor('autocomplete to be visible', () => PageObjects.console.hasAutocompleter()); + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.hasAutocompleter() + ); }); }); } diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts new file mode 100644 index 0000000000000..20fe9aeb1387a --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const panelActions = getService('dashboardPanelActions'); + + describe('embeddable library', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('unlink visualize panel from embeddable library', async () => { + // add heatmap panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.clickByButtonText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.existsByLinkText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save visualize panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:heatmap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + // add map panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.clickByButtonText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.existsByLinkText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save map panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:geomap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 9332503539874..b71a89501fbf6 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -81,6 +81,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); }); // Each of these tests call initTests themselves, the way it was originally written. The above tests only load diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index d13ba0114a598..e2a056359b48e 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -27,9 +27,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { + await esArchiver.load('management'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('management'); await PageObjects.settings.clickKibanaSavedObjects(); }); @@ -213,10 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('.json file', () => { beforeEach(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.load('saved_objects_imports'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('saved_objects_imports'); await PageObjects.settings.clickKibanaSavedObjects(); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index b2d27002f690c..eeb0b224d5f0c 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -12,10 +12,11 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); + const esArchiver = getService('esArchiver'); describe('index pattern filter', function describeIndexTests() { before(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.emptyKibanaIndex(); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index a58c129810470..90dd8cdc35c30 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('index pattern empty view', () => { before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.unload('logstash_functional'); await esArchiver.unload('makelogs'); await kibanaServer.uiSettings.replace({}); @@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); // @ts-expect-error await es.transport.request({ diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 27745654f495f..8697dc49de46a 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -18,14 +18,13 @@ export default function ({ getService, getPageObjects }) { describe('mgmt saved objects', function describeIndexTests() { beforeEach(async function () { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.load('discover'); await PageObjects.settings.navigateTo(); }); afterEach(async function () { await esArchiver.unload('discover'); - await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function () { diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 7ccce2c10c7b1..3102becbe181f 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 15828295b190f..d31245b5492d1 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -14,13 +14,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('management', function () { before(async () => { await esArchiver.unload('logstash_functional'); - await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); }); after(async () => { await esArchiver.unload('makelogs'); - await esArchiver.unload('empty_kibana'); }); describe('', function () { diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index 4f4e6d6655be5..caa008080b2a3 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const find = getService('find'); const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); @@ -53,7 +52,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/config.js b/test/functional/config.js index c15cfffbdb576..05d6cf9dd6b68 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,8 +32,10 @@ export default async function ({ readConfigFile }) { servers: commonConfig.get('servers'), - esTestCluster: commonConfig.get('esTestCluster'), - + esTestCluster: { + ...commonConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 534d4cebd92f4..881e3ad4157a4 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,8 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; +const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -170,6 +172,29 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } + async unlinkFromLibary(parent?: WebElementWrapper) { + log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await testSubjects.click('confirmSaveSavedObjectButton'); + } + async expectExistsRemovePanelAction() { log.debug('expectExistsRemovePanelAction'); await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index f28e219884bde..bd5ef814ae6c0 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -36,7 +36,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243..8d75cb09469bc 100644 --- a/test/plugin_functional/plugins/app_link_test/public/plugin.ts +++ b/test/plugin_functional/plugins/app_link_test/public/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { renderApp } from './app'; export class CoreAppLinkPlugin implements Plugin { - public async setup(core: CoreSetup, deps: {}) { + public setup(core: CoreSetup, deps: {}) { core.application.register({ id: 'applink_start', title: 'AppLink Start', diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 6a167b17befd1..48c8d85b21dac 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -42,7 +42,7 @@ export class CorePluginBPlugin }; } - public async start(core: CoreStart, deps: {}) { + public start(core: CoreStart, deps: {}) { return { sendSystemRequest: async (asSystemRequest: boolean) => { const response = await core.http.post('/core_plugin_b/system_request', { diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6c600aa996a33..0e52b536410e4 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const find = getService('find'); const retry = getService('retry'); const deployment = getService('deployment'); + const esArchiver = getService('esArchiver'); const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); @@ -50,6 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide describe('ui applications', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('foo'); }); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 918e9f16c5dae..7947616ac6568 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -12,8 +12,12 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('index patterns', function () { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); let indexPatternId = ''; it('can create an index pattern', async () => { diff --git a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts index 10a088426c8fd..da4c785342733 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts @@ -10,10 +10,15 @@ import path from 'path'; import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getPageObjects }: PluginFunctionalProviderContext) { +export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const esArchiver = getService('esArchiver'); describe('import warnings', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + beforeEach(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/test/scripts/jenkins_baseline.sh b/test/scripts/jenkins_baseline.sh index e679ac7f31bd1..60926238576c7 100755 --- a/test/scripts/jenkins_baseline.sh +++ b/test/scripts/jenkins_baseline.sh @@ -5,6 +5,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 6184708ea3fc6..5819a3ce6765e 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -17,6 +17,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + mkdir -p "$WORKSPACE/kibana-build-oss" cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index 7577b6927d166..aaacdd4ea3aae 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -6,6 +6,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index a9e603f63bd42..36865ce7c4967 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -32,6 +32,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting default Kibana distributable for use in functional tests" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts index 229dac20390a2..44a0e2eb0e121 100644 --- a/test/security_functional/insecure_cluster_warning.ts +++ b/test/security_functional/insecure_cluster_warning.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); await esArchiver.unload('hamlet'); + await esArchiver.emptyKibanaIndex(); }); it('should not warn when the cluster contains no user data', async () => { diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 7171a9b33bfd8..0ebb5c48033b8 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -27,7 +27,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index b305728b64de2..14381de6667fd 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 0c3e8ce78237a..d19883bcfe241 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 75a33226aa669..c4621500e927d 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index a120ea0b3a556..7f32bad648351 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1eb94af4dddf8..1d50bc7e05807 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -657,7 +657,7 @@ The following table describes the properties of the `incident` object. | externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | issueType | The id of the issue type in Jira. | string _(optional)_ | | priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | | parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 552053bdd7651..a81dfaeef8175 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -40,7 +40,15 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ externalId: schema.nullable(schema.string()), issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), parent: schema.nullable(schema.string()), }), comments: schema.nullable( diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d..8fbacc71d30cb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,9 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -136,11 +134,9 @@ const includedHiddenTypes = [ ALERT_SAVED_OBJECT_TYPE, ]; -export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly config: Promise; - +export class ActionsPlugin implements Plugin { private readonly logger: Logger; - private actionsConfig?: ActionsConfig; + private readonly actionsConfig: ActionsConfig; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -151,20 +147,20 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; - private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private readonly kibanaIndexConfig: { kibana: { index: string } }; constructor(initContext: PluginInitializerContext) { - this.config = initContext.config.create().pipe(first()).toPromise(); + this.actionsConfig = initContext.config.get(); this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; - this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; + this.kibanaIndexConfig = initContext.config.legacy.get(); } - public async setup( + public setup( core: CoreSetup, plugins: ActionsPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -190,7 +186,6 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - this.actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { @@ -229,20 +224,18 @@ export class ActionsPlugin implements Plugin, Plugi ); } - this.kibanaIndexConfig.subscribe((config) => { - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, config.kibana.index) + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, this.kibanaIndexConfig.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + this.kibanaIndexConfig.kibana.index ); - if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - config.kibana.index - ); - } - }); + } // Routes const router = core.http.createRouter(); @@ -304,7 +297,7 @@ export class ActionsPlugin implements Plugin, Plugi request ); - const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + const kibanaIndex = kibanaIndexConfig.kibana.index; return new ActionsClient({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index f0524e67d324c..da3afd03513f0 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -97,3 +97,4 @@ export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); export { APMPlugin, APMPluginSetup } from './plugin'; +export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e2840dbdf5ef7..49fded8649c46 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -61,7 +61,7 @@ export class APMPlugin implements Plugin { this.initContext = initContext; } - public async setup( + public setup( core: CoreSetup, plugins: { apmOss: APMOSSPluginSetup; @@ -98,7 +98,10 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = mergeConfigs( + plugins.apmOss.config, + this.initContext.config.get() + ); if ( plugins.taskManager && diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 6a814f68a67f4..3093d5d9b8d29 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { take } from 'rxjs/operators'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; @@ -27,14 +26,17 @@ interface StartDeps { } export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private readonly logger: Logger; private securitySetup?: SecurityPluginSetup; private beatsLibs?: CMServerLibs; constructor( private readonly initializerContext: PluginInitializerContext - ) {} + ) { + this.logger = initializerContext.logger.get(); + } - public async setup(core: CoreSetup, { features, security }: SetupDeps) { + public setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -64,8 +66,8 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return {}; } - public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = this.initializerContext.config.get(); const logger = this.initializerContext.logger.get(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -78,7 +80,9 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep kibanaVersion, }); - await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { + this.logger.error(`Error create beats template: ${e.message}`); + }); return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 7387ae1a203c2..345f6099009fc 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; @@ -34,7 +33,7 @@ export class CanvasPlugin implements Plugin { this.logger = initializerContext.logger.get(); } - public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); @@ -84,9 +83,7 @@ export class CanvasPlugin implements Plugin { ); // we need the kibana index provided by global config for the Canvas usage collector - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); setupInterpreter(plugins.expressions); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 5cfa4d70290f0..58d7c9abcbfd3 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -87,7 +87,7 @@ export const addComment = ({ // If the case is synced with alerts the newly attached alert must match the status of the case. if (newComment.attributes.type === CommentType.alert && myCase.attributes.settings.syncAlerts) { - caseClient.updateAlertsStatus({ + await caseClient.updateAlertsStatus({ ids: [newComment.attributes.alertId], status: myCase.attributes.status, }); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0..8b4fdc73dab44 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -38,8 +37,8 @@ import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; -function createConfig$(context: PluginInitializerContext) { - return context.config.create().pipe(map((config) => config)); +function createConfig(context: PluginInitializerContext) { + return context.config.get(); } export interface PluginsSetup { @@ -60,7 +59,7 @@ export class CasePlugin { } public async setup(core: CoreSetup, plugins: PluginsSetup) { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return; @@ -118,7 +117,7 @@ export class CasePlugin { }); } - public async start(core: CoreStart) { + public start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); this.alertsService!.initialize(core.elasticsearch.client); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index eeb295b264f60..4c12aa3d92b47 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -45,7 +45,7 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 55ed72ca01957..6abfb864d1cd0 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { CloudConfigType } from './config'; @@ -28,25 +26,24 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: CloudConfigType; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup, { usageCollection }: PluginsSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup) { this.logger.debug('Setting up Cloud plugin'); - const config = await this.config$.pipe(first()).toPromise(); - const isCloudEnabled = getIsCloudEnabled(config.id); + const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); return { - cloudId: config.id, + cloudId: this.config.id, isCloudEnabled, apm: { - url: config.apm?.url, - secretToken: config.apm?.secret_token, + url: this.config.apm?.url, + secretToken: this.config.apm?.secret_token, }, }; } diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index c9197a30b5214..eb7481d12387d 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, Plugin } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** * Represents Code Plugin instance that will be managed by the Kibana plugin system. */ -export class CodePlugin { +export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup() { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + const config = this.initializerContext.config.get>(); if (config && Object.keys(config).length > 0) { this.initializerContext.logger diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index b7d7b7c0e20d1..0a116545e6e36 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -6,6 +6,7 @@ */ import React from 'react'; +import moment from 'moment'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; @@ -86,6 +87,9 @@ export class DataEnhancedPlugin application: core.application, timeFilter: plugins.data.query.timefilter.timefilter, storage: this.storage, + disableSaveAfterSessionCompletesTimeout: moment + .duration(this.config.search.sessions.notTouchedTimeout) + .asMilliseconds(), }) ) ), diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 79e49050941be..3437920ed7c98 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { StubBrowserStorage } from '@kbn/test/jest'; import { render, waitFor, screen, act } from '@testing-library/react'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public/'; @@ -20,6 +20,8 @@ import { } from '../../../../../../../src/plugins/data/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; const coreStart = coreMock.createStart(); const dataStart = dataPluginMock.createStartContract(); @@ -30,6 +32,12 @@ const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked refreshInterval$); timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); +const disableSaveAfterSessionCompletesTimeout = 5 * 60 * 1000; + +function Container({ children }: { children?: ReactNode }) { + return {children}; +} + beforeEach(() => { storage = new Storage(new StubBrowserStorage()); refreshInterval$.next({ value: 0, pause: true }); @@ -47,8 +55,13 @@ test("shouldn't show indicator in case no active search session", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) await expect( @@ -69,8 +82,13 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); sessionService.isSessionStorageReady.mockImplementation(() => false); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) @@ -93,8 +111,13 @@ test('should show indicator in case there is an active search session', async () application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); await waitFor(() => getByTestId('searchSessionIndicator')); }); @@ -118,13 +141,20 @@ test('should be disabled in case uiConfig says so ', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); test('should be disabled during auto-refresh', async () => { @@ -135,19 +165,82 @@ test('should be disabled during auto-refresh', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).not.toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); act(() => { refreshInterval$.next({ value: 0, pause: false }); }); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); +}); + +describe('Completed inactivity', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + test('save should be disabled after completed and timeout', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + storage, + disableSaveAfterSessionCompletesTimeout, + }); + + render( + + + + ); + + await waitFor(() => screen.getByTestId('searchSessionIndicator')); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + state$.next(SearchSessionState.Completed); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + }); }); describe('tour steps', () => { @@ -167,8 +260,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); @@ -199,8 +297,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); const searchSessionIndicator = await rendered.findByTestId('searchSessionIndicator'); expect(searchSessionIndicator).toBeTruthy(); @@ -225,8 +328,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument(); @@ -242,8 +350,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index b572db7ebfd4c..3935b5bb2814b 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useRef } from 'react'; -import { debounce, distinctUntilChanged, map } from 'rxjs/operators'; -import { timer } from 'rxjs'; +import React, { useCallback, useState } from 'react'; +import { debounce, distinctUntilChanged, map, mapTo, switchMap } from 'rxjs/operators'; +import { merge, of, timer } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { SearchSessionIndicator, SearchSessionIndicatorRef } from '../search_session_indicator'; @@ -26,6 +26,11 @@ export interface SearchSessionIndicatorDeps { timeFilter: TimefilterContract; application: ApplicationStart; storage: IStorageWrapper; + /** + * Controls for how long we allow to save a session, + * after the last search in the session has completed + */ + disableSaveAfterSessionCompletesTimeout: number; } export const createConnectedSearchSessionIndicator = ({ @@ -33,6 +38,7 @@ export const createConnectedSearchSessionIndicator = ({ application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }: SearchSessionIndicatorDeps): React.FC => { const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter @@ -43,60 +49,104 @@ export const createConnectedSearchSessionIndicator = ({ debounce((_state) => timer(_state === SearchSessionState.None ? 50 : 300)) // switch to None faster to quickly remove indicator when navigating away ); + const disableSaveAfterSessionCompleteTimedOut$ = sessionService.state$.pipe( + switchMap((_state) => + _state === SearchSessionState.Completed + ? merge(of(false), timer(disableSaveAfterSessionCompletesTimeout).pipe(mapTo(true))) + : of(false) + ), + distinctUntilChanged() + ); + return () => { - const ref = useRef(null); const state = useObservable(debouncedSessionServiceState$, SearchSessionState.None); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); - const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const isSaveDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const disableSaveAfterSessionCompleteTimedOut = useObservable( + disableSaveAfterSessionCompleteTimedOut$, + false + ); + const [ + searchSessionIndicator, + setSearchSessionIndicator, + ] = useState(null); + const searchSessionIndicatorRef = useCallback((ref: SearchSessionIndicatorRef) => { + if (ref !== null) { + setSearchSessionIndicator(ref); + } + }, []); - let disabled = false; - let disabledReasonText: string = ''; + let saveDisabled = false; + let saveDisabledReasonText: string = ''; if (autoRefreshEnabled) { - disabled = true; - disabledReasonText = i18n.translate( + saveDisabled = true; + saveDisabledReasonText = i18n.translate( 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', { - defaultMessage: 'Search sessions are not available when auto refresh is enabled.', + defaultMessage: 'Saving search session is not available when auto refresh is enabled.', + } + ); + } + + if (disableSaveAfterSessionCompleteTimedOut) { + saveDisabled = true; + saveDisabledReasonText = i18n.translate( + 'xpack.data.searchSessionIndicator.disabledDueToTimeoutMessage', + { + defaultMessage: 'Search session results expired.', } ); } + if (isSaveDisabledByApp.disabled) { + saveDisabled = true; + saveDisabledReasonText = isSaveDisabledByApp.reasonText; + } + const { markOpenedDone, markRestoredDone } = useSearchSessionTour( storage, - ref, + searchSessionIndicator, state, - disabled + saveDisabled ); - if (isDisabledByApp.disabled) { - disabled = true; - disabledReasonText = isDisabledByApp.reasonText; - } + const onOpened = useCallback( + (openedState: SearchSessionState) => { + markOpenedDone(); + if (openedState === SearchSessionState.Restored) { + markRestoredDone(); + } + }, + [markOpenedDone, markRestoredDone] + ); + + const onContinueInBackground = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onSaveResults = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onCancel = useCallback(() => { + sessionService.cancel(); + }, []); if (!sessionService.isSessionStorageReady()) return null; return ( { - sessionService.save(); - }} - onSaveResults={() => { - sessionService.save(); - }} - onCancel={() => { - sessionService.cancel(); - }} - disabled={disabled} - disabledReasonText={disabledReasonText} - onOpened={(openedState) => { - markOpenedDone(); - if (openedState === SearchSessionState.Restored) { - markRestoredDone(); - } - }} + saveDisabled={saveDisabled} + saveDisabledReasonText={saveDisabledReasonText} + onContinueInBackground={onContinueInBackground} + onSaveResults={onSaveResults} + onCancel={onCancel} + onOpened={onOpened} /> ); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx index 8c04410f9953b..7987278f400ff 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { MutableRefObject, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { SearchSessionIndicatorRef } from '../search_session_indicator'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; @@ -16,7 +16,7 @@ export const TOUR_RESTORE_STEP_KEY = `data.searchSession.tour.restore`; export function useSearchSessionTour( storage: IStorageWrapper, - searchSessionIndicatorRef: MutableRefObject, + searchSessionIndicatorRef: SearchSessionIndicatorRef | null, state: SearchSessionState, searchSessionsDisabled: boolean ) { @@ -30,19 +30,20 @@ export function useSearchSessionTour( useEffect(() => { if (searchSessionsDisabled) return; + if (!searchSessionIndicatorRef) return; let timeoutHandle: number; if (state === SearchSessionState.Loading) { if (!safeHas(storage, TOUR_TAKING_TOO_LONG_STEP_KEY)) { timeoutHandle = window.setTimeout(() => { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); }, TOUR_TAKING_TOO_LONG_TIMEOUT); } } if (state === SearchSessionState.Restored) { if (!safeHas(storage, TOUR_RESTORE_STEP_KEY)) { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); } } @@ -79,15 +80,3 @@ function safeSet(storage: IStorageWrapper, key: string) { return true; } } - -function safeOpen(searchSessionIndicatorRef: MutableRefObject) { - if (searchSessionIndicatorRef.current) { - searchSessionIndicatorRef.current.openPopover(); - } else { - // TODO: needed for initial open when component is not rendered yet - // fix after: https://github.com/elastic/eui/issues/4460 - setTimeout(() => { - searchSessionIndicatorRef.current?.openPopover(); - }, 50); - } -} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx index f2d5a3c52daea..62d95c1043800 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -33,9 +33,9 @@ storiesOf('components/SearchSessionIndicator', module).add('default', () => (
diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index 59c39aecddb32..ff9e27cad1869 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -108,11 +108,21 @@ test('Canceled state', async () => { }); test('Disabled state', async () => { - render( + const { rerender } = render( + + + + ); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + + rerender( - + ); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index 9ac537829a670..eb58039ff58f7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -31,8 +31,10 @@ export interface SearchSessionIndicatorProps { onCancel?: () => void; viewSearchSessionsLink?: string; onSaveResults?: () => void; - disabled?: boolean; - disabledReasonText?: string; + + saveDisabled?: boolean; + saveDisabledReasonText?: string; + onOpened?: (openedState: SearchSessionState) => void; } @@ -55,17 +57,22 @@ const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonPro const ContinueInBackgroundButton = ({ onContinueInBackground = () => {}, buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, }: ActionButtonProps) => ( - - - + + + + + ); const ViewAllSearchSessionsButton = ({ @@ -84,17 +91,25 @@ const ViewAllSearchSessionsButton = ({ ); -const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - - - +const SaveButton = ({ + onSaveResults = () => {}, + buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, +}: ActionButtonProps) => ( + + + + + ); const searchSessionIndicatorViewStateToProps: { @@ -325,19 +340,16 @@ export const SearchSessionIndicator = React.forwardRef< className="searchSessionIndicator" data-test-subj={'searchSessionIndicator'} data-state={props.state} + data-save-disabled={props.saveDisabled ?? false} panelClassName={'searchSessionIndicator__panel'} repositionOnScroll={true} button={ - + } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 76235c917b139..3aaf50fbeb3e6 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { PluginSetup as DataPluginSetup, @@ -40,11 +39,11 @@ export class EnhancedDataServerPlugin implements Plugin { private readonly logger: Logger; private sessionService!: SearchSessionService; - private config$: Observable; + private config: ConfigSchema; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); - this.config$ = this.initializerContext.config.create(); + this.config = this.initializerContext.config.get(); } public setup(core: CoreSetup, deps: SetupDependencies) { @@ -56,7 +55,7 @@ export class EnhancedDataServerPlugin deps.data.search.registerSearchStrategy( ENHANCED_ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( - this.config$, + this.config, this.initializerContext.config.legacy.globalConfig$, this.logger, usage @@ -68,10 +67,7 @@ export class EnhancedDataServerPlugin eqlSearchStrategyProvider(this.logger) ); - this.sessionService = new SearchSessionService( - this.logger, - this.initializerContext.config.create() - ); + this.sessionService = new SearchSessionService(this.logger, this.config); deps.data.__enhance({ search: { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index 019b94f638ca4..d529e981aaea1 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -72,13 +72,13 @@ describe('ES search strategy', () => { }, }); - const mockConfig$ = new BehaviorSubject({ + const mockConfig: any = { search: { sessions: { defaultExpiration: moment.duration('1', 'm'), }, }, - }); + }; beforeEach(() => { mockApiCaller.mockClear(); @@ -89,7 +89,7 @@ describe('ES search strategy', () => { it('returns a strategy with `search and `cancel`', async () => { const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -104,7 +104,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -123,7 +123,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -142,7 +142,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -160,7 +160,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-程', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -189,7 +189,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -209,7 +209,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -237,7 +237,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -262,7 +262,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -287,7 +287,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -311,7 +311,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -338,7 +338,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -357,7 +357,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 402058a776605..fc1cc63146358 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -39,7 +39,7 @@ import { ConfigSchema } from '../../config'; import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; export const enhancedEsSearchStrategyProvider = ( - config$: Observable, + config: ConfigSchema, legacyConfig$: Observable, logger: Logger, usage?: SearchUsage @@ -60,7 +60,6 @@ export const enhancedEsSearchStrategyProvider = ( const client = esClient.asCurrentUser.asyncSearch; const search = async () => { - const config = await config$.pipe(first()).toPromise(); const params = id ? getDefaultAsyncGetParams(options) : { diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 75b6089cddf9b..8aa35def387b7 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Duration } from 'moment'; import { TaskManagerSetupContract, @@ -24,14 +22,13 @@ export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYP interface SearchSessionTaskDeps { taskManager: TaskManagerSetupContract; logger: Logger; - config$: Observable; + config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config$ }: SearchSessionTaskDeps) { +function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { return ({ taskInstance }: RunContext) => { return { async run() { - const config = await config$.pipe(first()).toPromise(); const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 19679f02df0ad..24d13cf24ccfb 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject } from 'rxjs'; import { SavedObject, SavedObjectsClientContract, @@ -46,7 +45,7 @@ describe('SearchSessionService', () => { beforeEach(async () => { savedObjectsClient = savedObjectsClientMock.create(); - const config$ = new BehaviorSubject({ + const config: ConfigSchema = { search: { sessions: { enabled: true, @@ -59,13 +58,13 @@ describe('SearchSessionService', () => { management: {} as any, }, }, - }); + }; const mockLogger: any = { debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger, config$); + service = new SearchSessionService(mockLogger, config); const coreStart = coreMock.createStart(); const mockTaskManager = taskManagerMock.createStart(); await flushPromises(); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 059edd5edf1de..2d0e7e519e3bd 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -50,32 +48,33 @@ function sleep(ms: number) { } export class SearchSessionService implements ISearchSessionService { - private config!: SearchSessionsConfig; + private sessionConfig: SearchSessionsConfig; - constructor( - private readonly logger: Logger, - private readonly config$: Observable - ) {} + constructor(private readonly logger: Logger, private readonly config: ConfigSchema) { + this.sessionConfig = this.config.search.sessions; + } public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { - config$: this.config$, + config: this.config, taskManager: deps.taskManager, logger: this.logger, }); } public async start(core: CoreStart, deps: StartDependencies) { - const configPromise = await this.config$.pipe(first()).toPromise(); - this.config = (await configPromise).search.sessions; return this.setupMonitoring(core, deps); } public stop() {} private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { - if (this.config.enabled) { - scheduleSearchSessionsTasks(deps.taskManager, this.logger, this.config.trackingInterval); + if (this.sessionConfig.enabled) { + scheduleSearchSessionsTasks( + deps.taskManager, + this.logger, + this.sessionConfig.trackingInterval + ); } }; @@ -107,7 +106,7 @@ export class SearchSessionService } catch (createError) { if ( SavedObjectsErrorHelpers.isConflictError(createError) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(createError); } else { @@ -116,7 +115,7 @@ export class SearchSessionService } } else if ( SavedObjectsErrorHelpers.isConflictError(e) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(e); } else { @@ -164,7 +163,7 @@ export class SearchSessionService sessionId, status: SearchSessionStatus.IN_PROGRESS, expires: new Date( - Date.now() + this.config.defaultExpiration.asMilliseconds() + Date.now() + this.sessionConfig.defaultExpiration.asMilliseconds() ).toISOString(), created: new Date().toISOString(), touched: new Date().toISOString(), diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 8097c22cfbabc..53b020e5b8241 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; @@ -15,4 +15,4 @@ export { EncryptedSavedObjectsClient } from './saved_objects'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 2324c31b13d00..823a6b0afa9dc 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; import { ConfigSchema } from './config'; import { coreMock } from 'src/core/server/mocks'; @@ -13,12 +13,12 @@ import { securityMock } from '../../security/server/mocks'; describe('EncryptedSavedObjects Plugin', () => { describe('setup()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) - .resolves.toMatchInlineSnapshot(` + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` Object { "createMigration": [Function], "registerType": [Function], @@ -29,14 +29,14 @@ describe('EncryptedSavedObjects Plugin', () => { }); describe('start()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); + plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); const startContract = plugin.start(); - await expect(startContract).toMatchInlineSnapshot(` + expect(startContract).toMatchInlineSnapshot(` Object { "getClient": [Function], "isEncryptionError": [Function], diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index bfc757accaa82..e846b133c26e0 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../security/server'; import { createConfig, ConfigSchema } from './config'; @@ -40,7 +39,9 @@ export interface EncryptedSavedObjectsPluginStart { /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class EncryptedSavedObjectsPlugin + implements + Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; @@ -48,17 +49,11 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( - core: CoreSetup, - deps: PluginsSetup - ): Promise { - const config = await this.initializerContext.config - .create>() - .pipe( - map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'))) - ) - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { + const config = createConfig( + this.initializerContext.config.get>(), + this.initializerContext.logger.get('config') + ); const auditLogger = new EncryptedSavedObjectsAuditLogger( deps.security?.audit.getLogger('encryptedSavedObjects') ); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4ea8ef2c089e4..569479f921cdd 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Plugin, PluginInitializerContext, @@ -66,19 +64,19 @@ export interface RouteDependencies { } export class EnterpriseSearchPlugin implements Plugin { - private config: Observable; - private logger: Logger; + private readonly config: ConfigType; + private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { - this.config = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.logger = initializerContext.logger.get(); } - public async setup( + public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { - const config = await this.config.pipe(first()).toPromise(); + const config = this.config; const log = this.logger; /** diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 04be4ce67c12d..9cc874735cc0e 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -24,7 +22,6 @@ import type { IEventLogConfig, IEventLogService, IEventLogger, - IEventLogConfig$, IEventLogClientService, } from './types'; import { findRoute } from './routes'; @@ -48,32 +45,29 @@ interface PluginStartDeps { } export class Plugin implements CorePlugin { - private readonly config$: IEventLogConfig$; + private readonly config: IEventLogConfig; private systemLogger: Logger; private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; - private globalConfig$: Observable; + private globalConfig: SharedGlobalConfig; private eventLogClientService?: EventLogClientService; private savedObjectProviderRegistry: SavedObjectProviderRegistry; private kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); - this.config$ = this.context.config.create(); - this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.config = this.context.config.get(); + this.globalConfig = this.context.config.legacy.get(); this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); this.kibanaVersion = this.context.env.packageInfo.version; } - async setup(core: CoreSetup): Promise { - const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); - const kibanaIndex = globalConfig.kibana.index; + setup(core: CoreSetup): IEventLogService { + const kibanaIndex = this.globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); - const config = await this.config$.pipe(first()).toPromise(); - this.esContext = createEsContext({ logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) @@ -85,7 +79,7 @@ export class Plugin implements CorePlugin { + start(core: CoreStart, { spaces }: PluginStartDeps): IEventLogClientService { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 786f5ba587d26..0e5e62b591290 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; @@ -25,7 +24,6 @@ export const ConfigSchema = schema.object({ }); export type IEventLogConfig = TypeOf; -export type IEventLogConfig$ = Observable>; // the object exposed by plugin.setup() export interface IEventLogService { diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 111f294b6ad55..0890274fed950 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -6,7 +6,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; // These exports are part of public Features plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -25,4 +25,4 @@ export { export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new FeaturesPlugin(initializerContext); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 4462edeed9510..0de03e54e1f79 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; describe('Features Plugin', () => { let initContext: ReturnType; @@ -31,7 +31,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); registerKibanaFeature({ id: 'baz', @@ -58,7 +58,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features with timelion when available', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); @@ -88,7 +88,7 @@ describe('Features Plugin', () => { }); it('registers kibana features with not hidden saved objects types', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -101,7 +101,7 @@ describe('Features Plugin', () => { }); it('returns registered elasticsearch features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); registerElasticsearchFeature({ id: 'baz', @@ -123,7 +123,7 @@ describe('Features Plugin', () => { }); it('registers a capabilities provider', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index e96c257516b98..6a9fd1da826a6 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreStart, SavedObjectsServiceStart, Logger, + Plugin, PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -59,7 +60,9 @@ interface TimelionSetupContract { /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class FeaturesPlugin + implements + Plugin, RecursiveReadonly> { private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; @@ -68,10 +71,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } - ): Promise> { + ): RecursiveReadonly { this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled; defineRoutes({ diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 57c42d887bc83..2e18d427272ce 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -130,8 +130,8 @@ interface AgentBase { enrolled_at: string; unenrolled_at?: string; unenrollment_started_at?: string; - upgraded_at?: string; - upgrade_started_at?: string; + upgraded_at?: string | null; + upgrade_started_at?: string | null; access_api_key_id?: string; default_api_key?: string; default_api_key_id?: string; @@ -155,3 +155,163 @@ export interface AgentSOAttributes extends AgentBase { current_error_events?: string; packages?: string[]; } + +// Generated from FleetServer schema.json + +/** + * An Elastic Agent that has enrolled into Fleet + */ +export interface FleetServerAgent { + /** + * The version of the document in the index + */ + _version?: number; + /** + * Shared ID + */ + shared_id?: string; + /** + * Type + */ + type: AgentType; + /** + * Active flag + */ + active: boolean; + /** + * Date/time the Elastic Agent enrolled + */ + enrolled_at: string; + /** + * Date/time the Elastic Agent unenrolled + */ + unenrolled_at?: string; + /** + * Date/time the Elastic Agent unenrolled started + */ + unenrollment_started_at?: string; + /** + * Date/time the Elastic Agent was last upgraded + */ + upgraded_at?: string | null; + /** + * Date/time the Elastic Agent started the current upgrade + */ + upgrade_started_at?: string | null; + /** + * ID of the API key the Elastic Agent must used to contact Fleet Server + */ + access_api_key_id?: string; + agent?: FleetServerAgentMetadata; + /** + * User provided metadata information for the Elastic Agent + */ + user_provided_metadata: AgentMetadata; + /** + * Local metadata information for the Elastic Agent + */ + local_metadata: AgentMetadata; + /** + * The policy ID for the Elastic Agent + */ + policy_id?: string; + /** + * The current policy revision_idx for the Elastic Agent + */ + policy_revision_idx?: number | null; + /** + * The current policy coordinator for the Elastic Agent + */ + policy_coordinator_idx?: number; + /** + * Date/time the Elastic Agent was last updated + */ + last_updated?: string; + /** + * Date/time the Elastic Agent checked in last time + */ + last_checkin?: string; + /** + * Lst checkin status + */ + last_checkin_status?: 'error' | 'online' | 'degraded' | 'updating'; + /** + * ID of the API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key_id?: string; + /** + * API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key?: string; + /** + * Date/time the Elastic Agent was last updated + */ + updated_at?: string; + /** + * Packages array + */ + packages?: string[]; + /** + * The last acknowledged action sequence number for the Elastic Agent + */ + action_seq_no?: number; +} +/** + * An Elastic Agent metadata + */ +export interface FleetServerAgentMetadata { + /** + * The unique identifier for the Elastic Agent + */ + id: string; + /** + * The version of the Elastic Agent + */ + version: string; + [k: string]: any; +} + +/** + * An Elastic Agent action + */ +export interface FleetServerAgentAction { + /** + * The unique identifier for action document + */ + _id?: string; + /** + * The action sequence number + */ + _seq_no?: number; + /** + * The unique identifier for the Elastic Agent action. There could be multiple documents with the same action_id if the action is split into two separate documents. + */ + action_id?: string; + /** + * Date/time the action was created + */ + '@timestamp'?: string; + /** + * The action expiration date/time + */ + expiration?: string; + /** + * The action type. APP_ACTION is the value for the actions that suppose to be routed to the endpoints/beats. + */ + type?: string; + /** + * The input identifier the actions should be routed to. + */ + input_id?: string; + /** + * The Agent IDs the action is intended for. No support for json.RawMessage with the current generator. Could be useful to lazy parse the agent ids + */ + agents?: string[]; + /** + * The opaque payload. + */ + data?: { + [k: string]: unknown; + }; + [k: string]: unknown; +} diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0f59befc2e467..e7e5a931b7429 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -221,6 +221,7 @@ export interface RegistryDataStream { path: string; ingest_pipeline: string; elasticsearch?: RegistryElasticsearch; + dataset_is_prefix?: boolean; } export interface RegistryElasticsearch { diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573d..50e647e271ecc 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -155,7 +155,7 @@ export class FleetPlugin implements Plugin { + public start(core: CoreStart): FleetStart { let successPromise: ReturnType; return { diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index a903de0138039..b34568b5fc6af 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -34,3 +34,4 @@ export class FleetAdminUserInvalidError extends IngestManagerError {} export class ConcurrentInstallOperationError extends IngestManagerError {} export class AgentReassignmentError extends IngestManagerError {} export class AgentUnenrollmentError extends IngestManagerError {} +export class AgentPolicyDeletionError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a34..7378d45e1bb3a 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -12,7 +12,7 @@ import { CoreStart, ElasticsearchServiceStart, Logger, - Plugin, + AsyncPlugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -169,7 +169,7 @@ export interface FleetStartContract { } export class FleetPlugin - implements Plugin { + implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; private cloud: CloudSetup | undefined; diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index 2d7c884edad83..22b5035378a20 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -20,7 +20,7 @@ export const postAgentAcksHandlerBuilder = function ( try { const soClient = ackService.getSavedObjectsClientContract(request); const esClient = ackService.getElasticsearchClientContract(); - const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, esClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index bf0cfd2d476dd..d032945245faf 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -30,7 +30,7 @@ export const postNewAgentActionHandlerBuilder = function ( const newAgentAction = request.body.action; - const savedAgentAction = await actionsService.createAgentAction(soClient, { + const savedAgentAction = await actionsService.createAgentAction(soClient, esClient, { created_at: new Date().toISOString(), ...newAgentAction, agent_id: agent.id, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 411da6da0223c..cd91e8c325c06 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -132,8 +132,8 @@ export const updateAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await AgentService.updateAgent(soClient, request.params.agentId, { - userProvidedMetatada: request.body.user_provided_metadata, + await AgentService.updateAgent(soClient, esClient, request.params.agentId, { + user_provided_metadata: request.body.user_provided_metadata, }); const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); @@ -164,12 +164,13 @@ export const postAgentCheckinHandler: RequestHandler< try { const soClient = appContextService.getInternalUserSOClient(request); const esClient = appContextService.getInternalUserESClient(); - const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, esClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); }); const signal = abortController.signal; + const { actions } = await AgentService.agentCheckin( soClient, esClient, @@ -205,8 +206,13 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = context.core.elasticsearch.client.asInternalUser; const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); - const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); + const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById( + soClient, + esClient, + apiKeyId + ); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { return response.unauthorized({ @@ -311,21 +317,16 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; try { - // Reassign by array of IDs - const result = Array.isArray(request.body.agents) - ? await AgentService.reassignAgents( - soClient, - esClient, - { agentIds: request.body.agents }, - request.body.policy_id - ) - : await AgentService.reassignAgents( - soClient, - esClient, - { kuery: request.body.agents }, - request.body.policy_id - ); - const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + const results = await AgentService.reassignAgents( + soClient, + esClient, + Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }, + request.body.policy_id + ); + + const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { return { ...acc, [so.id]: { diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 0215b8f27b393..086a9411f20b8 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -8,18 +8,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import semverCoerce from 'semver/functions/coerce'; -import { - AgentSOAttributes, - PostAgentUpgradeResponse, - PostBulkAgentUpgradeResponse, -} from '../../../common/types'; +import { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types'; import { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types'; import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { savedObjectToAgent } from '../../services/agents/saved_objects'; import { isAgentUpgradeable } from '../../../common/services'; +import { getAgent } from '../../services/agents'; export const postAgentUpgradeHandler: RequestHandler< TypeOf, @@ -27,6 +22,7 @@ export const postAgentUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -39,12 +35,8 @@ export const postAgentUpgradeHandler: RequestHandler< }, }); } - - const agentSO = await soClient.get( - AGENT_SAVED_OBJECT_TYPE, - request.params.agentId - ); - if (agentSO.attributes.unenrollment_started_at || agentSO.attributes.unenrolled_at) { + const agent = await getAgent(soClient, esClient, request.params.agentId); + if (agent.unenrollment_started_at || agent.unenrolled_at) { return response.customError({ statusCode: 400, body: { @@ -53,7 +45,6 @@ export const postAgentUpgradeHandler: RequestHandler< }); } - const agent = savedObjectToAgent(agentSO); if (!force && !isAgentUpgradeable(agent, kibanaVersion)) { return response.customError({ statusCode: 400, @@ -66,6 +57,7 @@ export const postAgentUpgradeHandler: RequestHandler< try { await AgentService.sendUpgradeAgentAction({ soClient, + esClient, agentId: request.params.agentId, version, sourceUri, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index dfe5c19bc417b..9800ddf95f7b2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -36,7 +36,7 @@ import { FleetServerPolicy, AGENT_POLICY_INDEX, } from '../../common'; -import { AgentPolicyNameExistsError } from '../errors'; +import { AgentPolicyNameExistsError, AgentPolicyDeletionError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; @@ -448,6 +448,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (agentPolicy.is_managed) { + throw new AgentPolicyDeletionError(`Cannot delete managed policy ${id}`); + } + const { defaultAgentPolicy: { id: defaultAgentPolicyId }, } = await this.ensureDefaultAgentPolicy(soClient, esClient); @@ -488,17 +492,17 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, agentPolicyId: string ) { - return appContextService.getConfig()?.agents.fleetServerEnabled - ? this.createFleetPolicyChangeFleetServer( - soClient, - appContextService.getInternalUserESClient(), - agentPolicyId - ) - : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + const esClient = appContextService.getInternalUserESClient(); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); + } + + return this.createFleetPolicyChangeActionSO(soClient, esClient, agentPolicyId); } public async createFleetPolicyChangeActionSO( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions @@ -518,7 +522,7 @@ class AgentPolicyService { return acc; }, []); - await createAgentPolicyAction(soClient, { + await createAgentPolicyAction(soClient, esClient, { type: 'POLICY_CHANGE', data: { policy }, ack_data: { packages }, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index c1a6067195c97..5aec696f5e144 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -106,19 +106,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -168,19 +168,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -230,8 +230,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { @@ -277,8 +277,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should fail for actions that cannot be found on agent actions list', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index a09107b90a015..c639a9b0332ac 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -24,14 +24,11 @@ import { AgentSOAttributes, AgentActionSOAttributes, } from '../../types'; -import { - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, -} from '../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionByIds } from './actions'; import { forceUnenrollAgent } from './unenroll'; import { ackAgentUpgraded } from './upgrade'; +import { updateAgent } from './crud'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -87,26 +84,23 @@ export async function acknowledgeAgentActions( const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); if (upgradeAction) { - await ackAgentUpgraded(soClient, upgradeAction); + await ackAgentUpgraded(soClient, esClient, upgradeAction); } const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); - await soClient.bulkUpdate([ - ...(configChangeAction - ? [ - { - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - policy_revision: configChangeAction.policy_revision, - packages: configChangeAction?.ack_data?.packages, - }, - }, - ] - : []), - ...buildUpdateAgentActionSentAt(agentActionsIds), - ]); + if (configChangeAction) { + await updateAgent(soClient, esClient, agent.id, { + policy_revision: configChangeAction.policy_revision, + packages: configChangeAction?.ack_data?.packages, + }); + } + + if (agentActionsIds.length > 0) { + await soClient.bulkUpdate([ + ...buildUpdateAgentActionSentAt(agentActionsIds), + ]); + } return actions; } @@ -206,6 +200,7 @@ export interface AcksService { authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ) => Promise; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 5b3c2ea5ce708..3d391cc89a7e3 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -8,12 +8,12 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; import { AgentAction } from '../../../common/types/models'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - + const mockedEsClient = elasticsearchServiceMock.createInternalClient(); const newAgentAction: Omit = { agent_id: 'agentid', type: 'POLICY_CHANGE', @@ -32,7 +32,7 @@ describe('test agent actions services', () => { }, } as SavedObject) ); - await createAgentAction(mockSavedObjectsClient, newAgentAction); + await createAgentAction(mockSavedObjectsClient, mockedEsClient, newAgentAction); const createdAction = (mockSavedObjectsClient.create.mock .calls[0][1] as unknown) as AgentAction; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index b45b7836eb46f..8dfeac11dacf3 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -13,8 +13,9 @@ import { BaseAgentActionSOAttributes, AgentActionSOAttributes, AgentPolicyActionSOAttributes, + FleetServerAgentAction, } from '../../../common/types/models'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_ACTIONS_INDEX } from '../../../common/constants'; import { isAgentActionSavedObject, isPolicyActionSavedObject, @@ -23,37 +24,45 @@ import { import { appContextService } from '../app_context'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; +const ONE_MONTH_IN_MS = 2592000000; + export async function createAgentAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } export async function bulkCreateAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise { - return bulkCreateActions(soClient, newAgentActions); + return bulkCreateActions(soClient, esClient, newAgentActions); } export function createAgentPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit | Omit ): Promise { const actionSO = await soClient.create( @@ -65,6 +74,27 @@ async function createAction( } ); + if ( + appContextService.getConfig()?.agents?.fleetServerEnabled && + isAgentActionSavedObject(actionSO) + ) { + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: newAgentAction.data, + type: newAgentAction.type, + }; + + await esClient.create({ + index: AGENT_ACTIONS_INDEX, + id: actionSO.id, + body, + refresh: 'wait_for', + }); + } + if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); // Action `data` is encrypted, so is not returned from the saved object @@ -84,14 +114,17 @@ async function createAction( async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array | Omit> ): Promise> { const { saved_objects: actionSOs } = await soClient.bulkCreate( @@ -105,6 +138,34 @@ async function bulkCreateActions( })) ); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await esClient.bulk({ + index: AGENT_ACTIONS_INDEX, + body: actionSOs.flatMap((actionSO) => { + if (!isAgentActionSavedObject(actionSO)) { + return []; + } + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: actionSO.attributes.data ? JSON.parse(actionSO.attributes.data) : undefined, + type: actionSO.type, + }; + + return [ + { + create: { + _id: actionSO.id, + }, + }, + body, + ]; + }), + }); + } + return actionSOs.map((actionSO) => { if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); @@ -316,6 +377,7 @@ export interface ActionsService { createAgentAction: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ) => Promise; } diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts index c59e2decebd99..5a1e86c15c002 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts @@ -6,10 +6,12 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { authenticateAgentWithAccessToken } from './authenticate'; +const mockEsClient = elasticsearchServiceMock.createInternalClient(); + describe('test agent autenticate services', () => { it('should succeed with a valid API key and an active agent', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); @@ -32,7 +34,7 @@ describe('test agent autenticate services', () => { ], }) ); - await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + await authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -62,7 +64,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: false }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -93,7 +95,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'aaaa', @@ -124,7 +126,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -144,7 +146,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.ts index a773173b1ddc1..a03c35bdc6e73 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.ts @@ -6,13 +6,14 @@ */ import Boom from '@hapi/boom'; -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { KibanaRequest, SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { Agent } from '../../types'; import * as APIKeyService from '../api_keys'; import { getAgentByAccessAPIKeyId } from './crud'; export async function authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise { if (!request.auth.isAuthenticated) { @@ -25,7 +26,7 @@ export async function authenticateAgentWithAccessToken( throw Boom.unauthorized(err.message); } - const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await getAgentByAccessAPIKeyId(soClient, esClient, res.apiKeyId); return agent; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 9a60abdc69423..bcebedae2e07a 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -19,9 +19,10 @@ import { AgentEventSOAttributes, } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { agentCheckinState } from './state'; import { getAgentActionsForCheckin } from '../actions'; +import { updateAgent } from '../crud'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -35,13 +36,7 @@ export async function agentCheckin( options?: { signal: AbortSignal } ) { const updateData: Partial = {}; - const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); - if ( - updatedErrorEvents && - !(updatedErrorEvents.length === 0 && agent.current_error_events.length === 0) - ) { - updateData.current_error_events = JSON.stringify(updatedErrorEvents); - } + await processEventsForCheckin(soClient, agent, data.events); if (data.localMetadata && !deepEqual(data.localMetadata, agent.local_metadata)) { updateData.local_metadata = data.localMetadata; } @@ -50,9 +45,8 @@ export async function agentCheckin( } // Update agent only if something changed if (Object.keys(updateData).length > 0) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); + await updateAgent(soClient, esClient, agent.id, updateData); } - // Check if some actions are not acknowledged let actions = await getAgentActionsForCheckin(soClient, agent.id); if (actions.length > 0) { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts index 83fd139a1e8e8..6156212a63203 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsBulkUpdateObject } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { appContextService } from '../../app_context'; -import { AgentSOAttributes } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { bulkUpdateAgents } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -57,20 +56,17 @@ export function agentCheckinStateConnectedAgentsFactory() { if (agentToUpdate.size === 0) { return; } + const esClient = appContextService.getInternalUserESClient(); const internalSOClient = getInternalUserSOClient(); const now = new Date().toISOString(); - const updates: Array> = [ - ...agentToUpdate.values(), - ].map((agentId) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: { + const updates = [...agentToUpdate.values()].map((agentId) => ({ + agentId, + data: { last_checkin: now, }, })); - agentToUpdate = new Set([...connectedAgentsIds.values()]); - await internalSOClient.bulkUpdate(updates, { refresh: false }); + await bulkUpdateAgents(internalSOClient, esClient, updates); } return { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts index ca8378c117b7d..cd6e0ef61e3f0 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { take } from 'rxjs/operators'; import { @@ -17,6 +18,7 @@ import { outputType } from '../../../../common/constants'; jest.mock('../../app_context', () => ({ appContextService: { + getConfig: () => ({}), getInternalUserSOClient: () => { return {}; }, @@ -42,6 +44,8 @@ function getMockedNewActionSince() { return getNewActionsSince as jest.MockedFunction; } +const mockedEsClient = {} as ElasticsearchClient; + describe('test agent checkin new action services', () => { describe('newAgetActionObservable', () => { beforeEach(() => { @@ -161,12 +165,18 @@ describe('test agent checkin new action services', () => { ]; expect( - await createAgentActionFromPolicyAction(mockSavedObjectsClient, mockAgent, mockPolicyAction) + await createAgentActionFromPolicyAction( + mockSavedObjectsClient, + mockedEsClient, + mockAgent, + mockPolicyAction + ) ).toEqual(expectedResult); expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -175,6 +185,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.2' } } } }, mockPolicyAction ) @@ -183,6 +194,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0' } } } }, mockPolicyAction ) @@ -191,6 +203,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -218,6 +231,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.0' } } } }, mockPolicyAction ) @@ -226,6 +240,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.3' } } } }, mockPolicyAction ) @@ -234,6 +249,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.1-SNAPSHOT' } } } }, mockPolicyAction ) @@ -242,6 +258,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.8.2' } } } }, mockPolicyAction ) diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 624b7bbcae572..01759c2015cdf 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -45,7 +45,7 @@ import { } from '../actions'; import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; -import { getAgent } from '../crud'; +import { getAgent, updateAgent } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -106,31 +106,45 @@ function createAgentPolicyActionSharedObservable(agentPolicyId: string) { ); } -async function getOrCreateAgentDefaultOutputAPIKey( +async function getAgentDefaultOutputAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent -): Promise { - const { - attributes: { default_api_key: defaultApiKey }, - } = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + return agent.default_api_key; + } else { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); - if (defaultApiKey) { return defaultApiKey; } +} + +async function getOrCreateAgentDefaultOutputAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agent: Agent +): Promise { + const defaultAPIKey = await getAgentDefaultOutputAPIKey(soClient, esClient, agent); + if (defaultAPIKey) { + return defaultAPIKey; + } const outputAPIKey = await APIKeysService.generateOutputApiKey(soClient, 'default', agent.id); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + await updateAgent(soClient, esClient, agent.id, { default_api_key: outputAPIKey.key, default_api_key_id: outputAPIKey.id, }); - return outputAPIKey.key; } export async function createAgentActionFromPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, policyAction: AgentPolicyAction ) { @@ -168,7 +182,7 @@ export async function createAgentActionFromPolicyAction( ); // Mutate the policy to set the api token for this agent - const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, agent); + const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, esClient, agent); if (newAgentAction.data.policy) { newAgentAction.data.policy.outputs.default.api_key = apiKey; } @@ -249,7 +263,9 @@ export function agentCheckinStateNewActionsFactory() { (!agent.policy_revision || action.policy_revision > agent.policy_revision) ), rateLimiter(), - concatMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), + concatMap((policyAction) => + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) + ), merge(newActions$), concatMap((data: AgentAction[] | undefined) => { if (data === undefined) { @@ -274,7 +290,7 @@ export function agentCheckinStateNewActionsFactory() { }), rateLimiter(), concatMap((policyAction) => - createAgentActionFromPolicyAction(soClient, agent, policyAction) + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) ) ); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 36506d0590595..c80fd77fc11ec 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -5,13 +5,8 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; - -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; import { appContextService, agentPolicyService } from '../../services'; import * as crudServiceSO from './crud_so'; import * as crudServiceFleetServer from './crud_fleet_server'; @@ -75,15 +70,15 @@ export async function getAgent( : crudServiceSO.getAgent(soClient, agentId); } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); - return agents; +export async function getAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentIds: string[] +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgents(esClient, agentIds) + : crudServiceSO.getAgents(soClient, agentIds); } export async function getAgentPolicyForAgent( @@ -104,38 +99,39 @@ export async function getAgentPolicyForAgent( export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), - }); - const [agent] = response.saved_objects.map(savedObjectToAgent); - - if (!agent) { - throw Boom.notFound('Agent not found'); - } - if (agent.access_api_key_id !== accessAPIKeyId) { - throw new Error('Agent api key id is not matching'); - } - if (!agent.active) { - throw Boom.forbidden('Agent inactive'); - } - - return agent; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgentByAccessAPIKeyId(esClient, accessAPIKeyId) + : crudServiceSO.getAgentByAccessAPIKeyId(soClient, accessAPIKeyId); } export async function updateAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.updateAgent(esClient, agentId, data) + : crudServiceSO.updateAgent(soClient, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: Array<{ + agentId: string; + data: Partial; + }> ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.bulkUpdateAgents(esClient, data) + : crudServiceSO.bulkUpdateAgents(soClient, data); } export async function deleteAgent( diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts index c9aa221edf4d2..caff15efff68c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -6,31 +6,46 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'src/core/server'; -import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { FleetServerAgent, isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; -import { searchHitToAgent } from './helpers'; +import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { appContextService } from '../../services'; +import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; const ACTIVE_AGENT_CONDITION = 'active:true'; const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } +function _joinFilters(filters: Array): KueryNode | undefined { + return filters + .filter((filter) => filter !== undefined) + .reduce((acc: KueryNode | undefined, kuery: string | KueryNode | undefined): + | KueryNode + | undefined => { + if (kuery === undefined) { + return acc; + } + const kueryNode: KueryNode = + typeof kuery === 'string' ? esKuery.fromKueryExpression(removeSOAttributes(kuery)) : kuery; - return `(${filter})`; - }, undefined); + if (!acc) { + return kueryNode; + } + + return { + type: 'function', + function: 'and', + arguments: [acc, kueryNode], + }; + }, undefined as KueryNode | undefined); } -function removeSOAttributes(kuery: string) { +export function removeSOAttributes(kuery: string) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } @@ -57,20 +72,23 @@ export async function listAgents( const filters = []; if (kuery && kuery !== '') { - filters.push(removeSOAttributes(kuery)); + filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, - q: _joinFilters(filters), + body, }); let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); @@ -121,18 +139,20 @@ export async function countInactiveAgents( filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, size: 0, track_total_hits: true, - q: _joinFilters(filters), + body, }); - return res.body.hits.total.value; } export async function getAgent(esClient: ElasticsearchClient, agentId: string) { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get>({ index: AGENTS_INDEX, id: agentId, }); @@ -141,27 +161,31 @@ export async function getAgent(esClient: ElasticsearchClient, agentId: string) { return agent; } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); +export async function getAgents( + esClient: ElasticsearchClient, + agentIds: string[] +): Promise { + const body = { docs: agentIds.map((_id) => ({ _id })) }; + + const res = await esClient.mget({ + body, + index: AGENTS_INDEX, + }); + + const agents = res.body.docs.map(searchHitToAgent); return agents; } export async function getAgentByAccessAPIKeyId( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), + const res = await esClient.search>({ + index: AGENTS_INDEX, + q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); - const [agent] = response.saved_objects.map(savedObjectToAgent); + + const [agent] = res.body.hits.hits.map(searchHitToAgent); if (!agent) { throw Boom.notFound('Agent not found'); @@ -177,15 +201,49 @@ export async function getAgentByAccessAPIKeyId( } export async function updateAgent( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, + await esClient.update({ + id: agentId, + index: AGENTS_INDEX, + body: { doc: agentSOAttributesToFleetServerAgentDoc(data) }, + refresh: 'wait_for', + }); +} + +export async function bulkUpdateAgents( + esClient: ElasticsearchClient, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const body = updateData.flatMap(({ agentId, data }) => [ + { + update: { + _id: agentId, + }, + }, + { + doc: { ...agentSOAttributesToFleetServerAgentDoc(data) }, + }, + ]); + + const res = await esClient.bulk({ + body, + index: AGENTS_INDEX, + refresh: 'wait_for', }); + + return { + items: res.body.items.map((item: { update: { _id: string; error?: Error } }) => ({ + id: item.update._id, + success: !item.update.error, + error: item.update.error, + })), + }; } export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { @@ -193,7 +251,7 @@ export async function deleteAgent(esClient: ElasticsearchClient, agentId: string id: agentId, index: AGENT_SAVED_OBJECT_TYPE, body: { - active: false, + doc: { active: false }, }, }); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts index 11991a971829a..c3ceb4b7502e2 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsBulkUpdateObject, SavedObjectsClientContract } from 'src/core/server'; import { isAgentUpgradeable } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -197,13 +197,35 @@ export async function getAgentByAccessAPIKeyId( export async function updateAgent( soClient: SavedObjectsClientContract, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const updates: Array> = updateData.map( + ({ agentId, data }) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agentId, + attributes: data, + }) + ); + + const res = await soClient.bulkUpdate(updates); + + return { + items: res.saved_objects.map((so) => ({ + id: so.id, + success: !so.error, + error: so.error, + })), + }; } export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index b8be02af101b4..c984a84ceea01 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -6,14 +6,15 @@ */ import Boom from '@hapi/boom'; +import uuid from 'uuid/v4'; import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes } from '../../types'; +import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import * as APIKeyService from '../api_keys'; import { appContextService } from '../app_context'; @@ -26,6 +27,36 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + const esClient = appContextService.getInternalUserESClient(); + + const agentId = uuid(); + const accessAPIKey = await APIKeyService.generateAccessApiKey(soClient, agentId); + const fleetServerAgent: FleetServerAgent = { + active: true, + policy_id: agentPolicyId, + type, + enrolled_at: new Date().toISOString(), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, + access_api_key_id: accessAPIKey.id, + }; + await esClient.create({ + index: AGENTS_INDEX, + body: fleetServerAgent, + id: agentId, + refresh: 'wait_for', + }); + + return { + id: agentId, + current_error_events: [], + packages: [], + ...fleetServerAgent, + access_api_key: accessAPIKey.key, + } as Agent; + } + const agentData: AgentSOAttributes = { active: true, policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 1000a1b145932..90d85e98ecd67 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -6,17 +6,30 @@ */ import { ESSearchHit } from '../../../../../typings/elasticsearch'; -import { Agent, AgentSOAttributes } from '../../types'; +import { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -export function searchHitToAgent(hit: ESSearchHit): Agent { +export function searchHitToAgent(hit: ESSearchHit): Agent { return { id: hit._id, ...hit._source, - current_error_events: hit._source.current_error_events - ? JSON.parse(hit._source.current_error_events) - : [], + policy_revision: hit._source.policy_revision_idx, + current_error_events: [], access_api_key: undefined, status: undefined, packages: hit._source.packages ?? [], }; } + +export function agentSOAttributesToFleetServerAgentDoc( + data: Partial +): Partial> { + const { policy_revision: policyRevison, ...rest } = data; + + const doc: Partial> = { ...rest }; + + if (policyRevison !== undefined) { + doc.policy_revision_idx = policyRevison; + } + + return doc; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 7338c440483ea..466870bead71c 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -107,6 +107,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 9f4373ab553ec..62d59aada3b7b 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -7,11 +7,16 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import type { AgentSOAttributes } from '../../types'; -import { AgentReassignmentError } from '../../errors'; import { agentPolicyService } from '../agent_policy'; -import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgents, + getAgentPolicyForAgent, + listAllAgents, + updateAgent, + bulkUpdateAgents, +} from './crud'; +import { AgentReassignmentError } from '../../errors'; + import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -27,12 +32,12 @@ export async function reassignAgent( await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { policy_id: newAgentPolicyId, policy_revision: null, }); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: new Date().toISOString(), type: 'INTERNAL_POLICY_REASSIGN', @@ -73,7 +78,7 @@ export async function reassignAgents( kuery: string; }, newAgentPolicyId: string -) { +): Promise<{ items: Array<{ id: string; sucess: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); @@ -82,7 +87,7 @@ export async function reassignAgents( // Filter to agents that do not already use the new agent policy ID const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -99,20 +104,22 @@ export async function reassignAgents( (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId ); - // Update the necessary agents - const res = await soClient.bulkUpdate( + const res = await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { policy_id: newAgentPolicyId, policy_revision: null, }, })) ); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c75b91b3fbd11..42d3aff2b0d70 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -14,6 +14,8 @@ import { AgentStatus } from '../../types'; import { AgentStatusKueryHelper } from '../../../common/services'; import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; import { normalizeKuery } from '../saved_object'; +import { appContextService } from '../app_context'; +import { removeSOAttributes } from './crud_fleet_server'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, @@ -27,6 +29,8 @@ export async function getAgentStatusById( export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; function joinKuerys(...kuerys: Array) { + const isFleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return kuerys .filter((kuery) => kuery !== undefined) .reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => { @@ -34,7 +38,9 @@ function joinKuerys(...kuerys: Array) { return acc; } const normalizedKuery: KueryNode = esKuery.fromKueryExpression( - normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') + isFleetServerEnabled + ? removeSOAttributes(kuery || '') + : normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') ); if (!acc) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index b8c1b7befb443..cd46cff0f8a17 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -73,6 +73,7 @@ describe('unenrollAgents (plural)', () => { }); it('cannot unenroll from a managed policy', async () => { const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id]; await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); @@ -98,6 +99,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index e2fa83cf32b63..72d551a122980 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { AgentSOAttributes } from '../../types'; -import { AgentUnenrollmentError } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import * as APIKeyService from '../api_keys'; import { createAgentAction, bulkCreateAgentActions } from './actions'; -import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgent, + updateAgent, + getAgentPolicyForAgent, + getAgents, + listAllAgents, + bulkUpdateAgents, +} from './crud'; +import { AgentUnenrollmentError } from '../../errors'; async function unenrollAgentIsAllowed( soClient: SavedObjectsClientContract, @@ -35,12 +41,12 @@ export async function unenrollAgent( await unenrollAgentIsAllowed(soClient, esClient, agentId); const now = new Date().toISOString(); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, type: 'UNENROLL', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { unenrollment_started_at: now, }); } @@ -58,7 +64,7 @@ export async function unenrollAgents( ) { const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -83,6 +89,7 @@ export async function unenrollAgents( // Create unenroll action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -91,11 +98,12 @@ export async function unenrollAgents( ); // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { unenrollment_started_at: now, }, })) @@ -118,7 +126,7 @@ export async function forceUnenrollAgent( : undefined, ]); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { active: false, unenrolled_at: new Date().toISOString(), }); @@ -138,7 +146,7 @@ export async function forceUnenrollAgents( // Filter to agents that are not already unenrolled const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -163,13 +171,13 @@ export async function forceUnenrollAgents( if (apiKeys.length) { APIKeyService.invalidateAPIKeys(soClient, apiKeys); } - // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { active: false, unenrolled_at: now, }, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 7475ad4968142..5105e14530982 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,20 +6,22 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentAction, AgentActionSOAttributes } from '../../types'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; -import { getAgents, listAllAgents } from './crud'; +import { getAgents, listAllAgents, updateAgent, bulkUpdateAgents } from './crud'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, + esClient, agentId, version, sourceUri, }: { soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; agentId: string; version: string; sourceUri: string | undefined; @@ -29,21 +31,22 @@ export async function sendUpgradeAgentAction({ version, source_uri: sourceUri, }; - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, data, ack_data: data, type: 'UPGRADE', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - upgraded_at: undefined, + await updateAgent(soClient, esClient, agentId, { + upgraded_at: null, upgrade_started_at: now, }); } export async function ackAgentUpgraded( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentAction: AgentAction ) { const { @@ -52,9 +55,9 @@ export async function ackAgentUpgraded( if (!ackData) throw new Error('data missing from UPGRADE action'); const { version } = JSON.parse(ackData); if (!version) throw new Error('version missing from UPGRADE action'); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, { + await updateAgent(soClient, esClient, agentAction.agent_id, { upgraded_at: new Date().toISOString(), - upgrade_started_at: undefined, + upgrade_started_at: null, }); } @@ -79,7 +82,7 @@ export async function sendUpgradeAgentsActions( // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -97,6 +100,7 @@ export async function sendUpgradeAgentsActions( // Create upgrade action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -106,12 +110,13 @@ export async function sendUpgradeAgentsActions( })) ); - return await soClient.bulkUpdate( + return await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - upgraded_at: undefined, + agentId: agent.id, + data: { + upgraded_at: null, upgrade_started_at: now, }, })) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 97d48702cf4c6..85812fee3885c 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -79,3 +79,15 @@ export async function generateEnrollmentAPIKey( return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } } + +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + apiKeyId: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKeyById(esClient, apiKeyId); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKeyById(soClient, apiKeyId); + } +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts index d42cb19a340bd..f5d0015297daa 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -7,41 +7,15 @@ import uuid from 'uuid'; import Boom from '@hapi/boom'; +import { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { ESSearchResponse as SearchResponse } from '../../../../../typings/elasticsearch'; import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; - -// TODO Move these types to another file -interface SearchResponse { - took: number; - timed_out: boolean; - _scroll_id?: string; - hits: { - total: { - value: number; - relation: string; - }; - max_score: number; - hits: Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; - }>; - }; -} - -type SearchHit = SearchResponse['hits']['hits'][0]; +import { escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( esClient: ElasticsearchClient, @@ -54,7 +28,7 @@ export async function listEnrollmentApiKeys( ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; - const res = await esClient.search>({ + const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -78,7 +52,7 @@ export async function getEnrollmentAPIKey( id: string ): Promise { try { - const res = await esClient.get>({ + const res = await esClient.get>({ index: ENROLLMENT_API_KEYS_INDEX, id, }); @@ -185,6 +159,21 @@ export async function generateEnrollmentAPIKey( }; } +export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, + }); + + const [enrollmentAPIKey] = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { try { await agentPolicyService.get(soClient, agentPolicyId); @@ -196,7 +185,10 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } -function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { +function esDocToEnrollmentApiKey(doc: { + _id: string; + _source: FleetServerEnrollmentAPIKey; +}): EnrollmentAPIKey { return { id: doc._id, ...doc._source, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts index b3beab546c811..014bc58e747ea 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -13,7 +13,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import { normalizeKuery, escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -159,6 +159,25 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + apiKeyId: string +) { + const [enrollmentAPIKey] = ( + await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + searchFields: ['api_key_id'], + search: escapeSearchQueryPhrase(apiKeyId), + }) + ).saved_objects.map(savedObjectToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + function savedObjectToEnrollmentApiKey({ error, attributes, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index 5cdadeb0c82d8..65051163c78c3 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core/server'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; +import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { createAPIKey } from './security'; -import { escapeSearchQueryPhrase } from '../saved_object'; export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; @@ -70,25 +67,6 @@ export async function generateAccessApiKey(soClient: SavedObjectsClientContract, return { id: key.id, key: Buffer.from(`${key.id}:${key.api_key}`).toString('base64') }; } -export async function getEnrollmentAPIKeyById( - soClient: SavedObjectsClientContract, - apiKeyId: string -) { - const [enrollmentAPIKey] = ( - await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - searchFields: ['api_key_id'], - search: escapeSearchQueryPhrase(apiKeyId), - }) - ).saved_objects.map(_savedObjectToEnrollmentApiKey); - - if (enrollmentAPIKey?.api_key_id !== apiKeyId) { - throw new Error('find enrollmentKeyById returned an incorrect key'); - } - - return enrollmentAPIKey; -} - export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; @@ -117,18 +95,3 @@ export function parseApiKey(apiKey: string) { apiKeyId, }; } - -function _savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; -} diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index 3e1d3d57bbf71..7ab904b2f15e1 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -22,10 +22,23 @@ password: {{password}} {{#if password}} hidden_password: {{password}} {{/if}} +{{#if optional_field}} +optional_field: {{optional_field}} +{{/if}} +foo: {{bar}} +some_text_field: {{should_be_text}} +multi_text_field: +{{#each multi_text}} + - {{this}} +{{/each}} `; const vars = { paths: { value: ['/usr/local/var/log/nginx/access.log'] }, password: { type: 'password', value: '' }, + optional_field: { type: 'text', value: undefined }, + bar: { type: 'text', value: 'bar' }, + should_be_text: { type: 'text', value: '1234' }, + multi_text: { type: 'text', value: ['1234', 'foo', 'bar'] }, }; const output = compileTemplate(vars, streamTemplate); @@ -35,6 +48,9 @@ hidden_password: {{password}} exclude_files: ['.gz$'], processors: [{ add_locale: null }], password: '', + foo: 'bar', + some_text_field: '1234', + multi_text_field: ['1234', 'foo', 'bar'], }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 6b1d84ea28b0a..4f39da5b0b70d 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -58,6 +58,10 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) return yaml; } +const maybeEscapeNumericString = (value: string) => { + return value.length && !isNaN(+value) ? `"${value}"` : value; +}; + function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateStr: string) { const yamlValues: { [k: string]: any } = {}; const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { @@ -84,6 +88,14 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt const yamlKeyPlaceholder = `##${key}##`; varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else if (recordEntry.type && recordEntry.type === 'text' && recordEntry.value?.length) { + if (Array.isArray(recordEntry.value)) { + varPart[lastKeyPart] = recordEntry.value.map((value: string) => + maybeEscapeNumericString(value) + ); + } else { + varPart[lastKeyPart] = maybeEscapeNumericString(recordEntry.value); + } } else { varPart[lastKeyPart] = recordEntry.value; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts new file mode 100644 index 0000000000000..be9213aff360d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.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 { RegistryDataStream } from '../../../../types'; +import { Field } from '../../fields/field'; + +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { installTemplate } from './install'; + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixFalse, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixTrue, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 10e94d93bbc8e..f5f1b4bea788d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -17,7 +17,13 @@ import { import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; -import { generateMappings, generateTemplateName, getTemplate } from './template'; +import { + generateMappings, + generateTemplateName, + generateTemplateIndexPattern, + getTemplate, + getTemplatePriority, +} from './template'; import { getAsset, getPathParts } from '../../archive'; import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; @@ -293,6 +299,9 @@ export async function installTemplate({ }): Promise { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataStream); + const templateIndexPattern = generateTemplateIndexPattern(dataStream); + const templatePriority = getTemplatePriority(dataStream); + let pipelineName; if (dataStream.ingest_pipeline) { pipelineName = getPipelineNameForInstallation({ @@ -310,11 +319,12 @@ export async function installTemplate({ const template = getTemplate({ type: dataStream.type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy: dataStream.ilm_policy, hidden: dataStream.hidden, }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index 80386a2a0dd56..a176805307845 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -8,8 +8,14 @@ import { readFileSync } from 'fs'; import { safeLoad } from 'js-yaml'; import path from 'path'; +import { RegistryDataStream } from '../../../../types'; import { Field, processFields } from '../../fields/field'; -import { generateMappings, getTemplate } from './template'; +import { + generateMappings, + getTemplate, + getTemplatePriority, + generateTemplateIndexPattern, +} from './template'; // Add our own serialiser to just do JSON.stringify expect.addSnapshotSerializer({ @@ -23,16 +29,17 @@ expect.addSnapshotSerializer({ }); test('get template', () => { - const templateName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); - expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); + expect(template.index_patterns).toStrictEqual([templateIndexPattern]); }); test('adds composed_of correctly', () => { @@ -40,10 +47,11 @@ test('adds composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); @@ -53,35 +61,36 @@ test('adds empty composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); test('adds hidden field correctly', () => { - const templateWithHiddenName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const templateWithHidden = getTemplate({ type: 'logs', - templateName: templateWithHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, hidden: true, }); expect(templateWithHidden.data_stream.hidden).toEqual(true); - const templateWithoutHiddenName = 'logs-nginx-access-efgh'; - const templateWithoutHidden = getTemplate({ type: 'logs', - templateName: templateWithoutHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); }); @@ -95,10 +104,11 @@ test('tests loading base.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'nginx', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -113,10 +123,11 @@ test('tests loading coredns.logs.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'coredns', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -131,10 +142,11 @@ test('tests loading system.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'metrics', - templateName: 'whatsthis', + templateIndexPattern: 'whatsthis-*', packageName: 'system', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -520,3 +532,62 @@ test('tests constant_keyword field type handling', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); }); + +test('tests priority and index pattern for data stream without dataset_is_prefix', () => { + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixUnset); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixUnset); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixUnset); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixUnset); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to false', () => { + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixFalse); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixFalse); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixFalse); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixFalse); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to true', () => { + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixTrue); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixTrue); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixTrue); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixTrue); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index ea0bb5dc53a1e..b86c989f8c24c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -33,6 +33,10 @@ export interface CurrentDataStream { const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; +// see discussion in https://github.com/elastic/kibana/issues/88307 +const DEFAULT_TEMPLATE_PRIORITY = 200; +const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; + /** * getTemplate retrieves the default template but overwrites the index pattern with the given value. * @@ -40,29 +44,32 @@ const DEFAULT_IGNORE_ABOVE = 1024; */ export function getTemplate({ type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden, }: { type: string; - templateName: string; + templateIndexPattern: string; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; composedOfTemplates: string[]; + templatePriority: number; ilmPolicy?: string | undefined; hidden?: boolean; }): IndexTemplate { const template = getBaseTemplate( type, - templateName, + templateIndexPattern, mappings, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden ); @@ -242,6 +249,35 @@ export function generateTemplateName(dataStream: RegistryDataStream): string { return getRegistryDataStreamAssetBaseName(dataStream); } +export function generateTemplateIndexPattern(dataStream: RegistryDataStream): string { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return getRegistryDataStreamAssetBaseName(dataStream) + '-*'; + } else { + return getRegistryDataStreamAssetBaseName(dataStream) + '.*-*'; + } +} + +// Template priorities are discussed in https://github.com/elastic/kibana/issues/88307 +// See also https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html +// +// Built-in templates like logs-*-* and metrics-*-* have priority 100 +// +// EPM generated templates for data streams have priority 200 (DEFAULT_TEMPLATE_PRIORITY) +// +// EPM generated templates for data streams with dataset_is_prefix: true have priority 150 (DATASET_IS_PREFIX_TEMPLATE_PRIORITY) + +export function getTemplatePriority(dataStream: RegistryDataStream): number { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return DEFAULT_TEMPLATE_PRIORITY; + } else { + return DATASET_IS_PREFIX_TEMPLATE_PRIORITY; + } +} + /** * Returns a map of the data stream path fields to elasticsearch index pattern. * @param dataStreams an array of RegistryDataStream objects @@ -255,17 +291,18 @@ export function generateESIndexPatterns( const patterns: Record = {}; for (const dataStream of dataStreams) { - patterns[dataStream.path] = generateTemplateName(dataStream) + '-*'; + patterns[dataStream.path] = generateTemplateIndexPattern(dataStream); } return patterns; } function getBaseTemplate( type: string, - templateName: string, + templateIndexPattern: string, mappings: IndexTemplateMappings, packageName: string, composedOfTemplates: string[], + templatePriority: number, ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { @@ -279,13 +316,9 @@ function getBaseTemplate( }; return { - // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) - // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream - // is created. I'm using 200 here to give some room for users to create their own template and fit it between the - // default and the one the ingest manager uses. - priority: 200, + priority: templatePriority, // To be completed with the correct index patterns - index_patterns: [`${templateName}-*`], + index_patterns: [templateIndexPattern], template: { settings: { index: { diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index f982332886e94..170bec54983c0 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -5,11 +5,18 @@ * 2.0. */ +import { isBoom } from '@hapi/boom'; import { KibanaRequest } from 'src/core/server'; import { ENROLLMENT_API_KEYS_INDEX, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + AGENT_POLICY_INDEX, + AGENTS_INDEX, FleetServerEnrollmentAPIKey, + AGENT_SAVED_OBJECT_TYPE, + AgentSOAttributes, + FleetServerAgent, + SO_SEARCH_LIMIT, FLEET_SERVER_PACKAGE, FLEET_SERVER_INDICES, } from '../../common'; @@ -17,6 +24,9 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollmen import { appContextService } from './app_context'; import { getInstallation } from './epm/packages'; +import { isAgentsSetup } from './agents'; +import { agentPolicyService } from './agent_policy'; + export async function isFleetServerSetup() { const pkgInstall = await getInstallation({ savedObjectsClient: getInternalUserSOClient(), @@ -28,7 +38,6 @@ export async function isFleetServerSetup() { } const esClient = appContextService.getInternalUserESClient(); - const exists = await Promise.all( FLEET_SERVER_INDICES.map(async (index) => { const res = await esClient.indices.exists({ @@ -42,7 +51,11 @@ export async function isFleetServerSetup() { } export async function runFleetServerMigration() { - await migrateEnrollmentApiKeys(); + // If Agents are not setup skip as there is nothing to migrate + if (!(await isAgentsSetup(getInternalUserSOClient()))) { + return; + } + await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); } function getInternalUserSOClient() { @@ -64,6 +77,65 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } +async function migrateAgents() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + page: 1, + perPage: 100, + }); + + if (res.total === 0) { + hasMore = false; + } + for (const so of res.saved_objects) { + try { + const { + attributes, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + + const body: FleetServerAgent = { + type: attributes.type, + active: attributes.active, + enrolled_at: attributes.enrolled_at, + unenrolled_at: attributes.unenrolled_at, + unenrollment_started_at: attributes.unenrollment_started_at, + upgraded_at: attributes.upgraded_at, + upgrade_started_at: attributes.upgrade_started_at, + access_api_key_id: attributes.access_api_key_id, + user_provided_metadata: attributes.user_provided_metadata, + local_metadata: attributes.local_metadata, + policy_id: attributes.policy_id, + policy_revision_idx: attributes.policy_revision || undefined, + last_checkin: attributes.last_checkin, + last_checkin_status: attributes.last_checkin_status, + default_api_key_id: attributes.default_api_key_id, + default_api_key: attributes.default_api_key, + packages: attributes.packages, + }; + await esClient.create({ + index: AGENTS_INDEX, + body, + id: so.id, + refresh: 'wait_for', + }); + + await soClient.delete(AGENT_SAVED_OBJECT_TYPE, so.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } + } + } +} + async function migrateEnrollmentApiKeys() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); @@ -77,24 +149,61 @@ async function migrateEnrollmentApiKeys() { hasMore = false; } for (const item of res.items) { - const key = await getEnrollmentAPIKey(soClient, item.id); - - const body: FleetServerEnrollmentAPIKey = { - api_key: key.api_key, - api_key_id: key.api_key_id, - active: key.active, - created_at: key.created_at, - name: key.name, - policy_id: key.policy_id, - }; - await esClient.create({ - index: ENROLLMENT_API_KEYS_INDEX, - body, - id: key.id, - refresh: 'wait_for', - }); + try { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); - await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } } } } + +async function migrateAgentPolicies() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + const { items: agentPolicies } = await agentPolicyService.list(soClient, { + perPage: SO_SEARCH_LIMIT, + }); + + await Promise.all( + agentPolicies.map(async (agentPolicy) => { + const res = await esClient.search({ + index: AGENT_POLICY_INDEX, + q: `policy_id:${agentPolicy.id}`, + track_total_hits: true, + }); + + if (res.body.hits.total.value === 0) { + return agentPolicyService.createFleetPolicyChangeFleetServer( + soClient, + esClient, + agentPolicy.id + ); + } + }) + ); +} + +function is404Error(error: any) { + return isBoom(error) && error.output.statusCode === 404; +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 9999ab91e31b2..77ce882275b6b 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -49,6 +49,7 @@ export interface AgentService { */ authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise; /** diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index ab24c26e0cdfa..f19ad4e7fe417 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -93,6 +93,16 @@ async function createSetupSideEffects( await runFleetServerMigration(); } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: FLEET_SERVER_PACKAGE, + callCluster, + }); + await ensureFleetServerIndicesCreated(esClient); + await runFleetServerMigration(); + } + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 0c35fc29e01cd..fda1568c56e0e 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -81,6 +81,9 @@ export { dataTypes, // Fleet Server types FleetServerEnrollmentAPIKey, + FleetServerAgent, + FleetServerAgentAction, + FleetServerPolicy, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 8d560d9a0f553..d7c06a92f70e0 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { LicensingPluginStart } from '../../licensing/server'; import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; @@ -33,20 +31,19 @@ export class GlobalSearchPlugin GlobalSearchPluginSetupDeps, GlobalSearchPluginStartDeps > { - private readonly config$: Observable; + private readonly config: GlobalSearchConfigType; private readonly searchService = new SearchService(); private searchServiceStart?: SearchServiceStart; private licenseChecker?: ILicenseChecker; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { - const config = await this.config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { const { registerResultProvider } = this.searchService.setup({ basePath: core.http.basePath, - config, + config: this.config, }); registerRoutes(core.http.createRouter()); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 5c13756842039..32dac5fba86f9 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -20,7 +20,7 @@ import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; - public async setup( + public setup( core: CoreSetup, { licensing, diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 532cf253c7f89..95793c0cad465 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -52,22 +50,19 @@ const indexLifecycleDataEnricher = async ( }; export class IndexLifecycleManagementServerPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: IndexLifecycleManagementConfig; private readonly license: License; private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.license = new License(); } - async setup( - { http }: CoreSetup, - { licensing, indexManagement, features }: Dependencies - ): Promise { + setup({ http }: CoreSetup, { licensing, indexManagement, features }: Dependencies): void { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); + const config = this.config; this.license.setup( { diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 76533a476561b..e6baca305508e 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -41,7 +41,21 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< typeof getLogAlertsChartPreviewDataSuccessResponsePayloadRT >; -export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ +// This should not have an explicit `any` return type, but it's here because its +// inferred type includes `Comparator` which is a string enum exported from +// common/alerting/logs/log_threshold/types.ts. +// +// There's a bug that's fixed in TypeScript 4.2.0 that will allow us to remove +// the `:any` from this, so remove it when that update happens. +// +// If it's removed before then you get: +// +// x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts:44:14 - error TS4023: +// Exported variable 'getLogAlertsChartPreviewDataAlertParamsSubsetRT' has or is using name 'Comparator' +// from external module "/Users/smith/Code/kibana/x-pack/plugins/infra/common/alerting/logs/log_threshold/types" +// but cannot be named. +// +export const getLogAlertsChartPreviewDataAlertParamsSubsetRT: any = rt.intersection([ rt.type({ criteria: countCriteriaRT, timeUnit: timeUnitRT, diff --git a/x-pack/plugins/infra/common/http_api/shared/errors.ts b/x-pack/plugins/infra/common/http_api/shared/errors.ts index 5e439c31bbdc9..2b5461d71500e 100644 --- a/x-pack/plugins/infra/common/http_api/shared/errors.ts +++ b/x-pack/plugins/infra/common/http_api/shared/errors.ts @@ -7,18 +7,35 @@ import * as rt from 'io-ts'; -const createErrorRuntimeType = ( - statusCode: number, - errorCode: string, - attributes?: Attributes -) => +export const badRequestErrorRT = rt.intersection([ rt.type({ - statusCode: rt.literal(statusCode), - error: rt.literal(errorCode), + statusCode: rt.literal(400), + error: rt.literal('Bad Request'), message: rt.string, - ...(!!attributes ? { attributes } : {}), - }); + }), + rt.partial({ + attributes: rt.unknown, + }), +]); -export const badRequestErrorRT = createErrorRuntimeType(400, 'Bad Request'); -export const forbiddenErrorRT = createErrorRuntimeType(403, 'Forbidden'); -export const conflictErrorRT = createErrorRuntimeType(409, 'Conflict'); +export const forbiddenErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(403), + error: rt.literal('Forbidden'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); + +export const conflictErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(409), + error: rt.literal('Conflict'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 2d3b6a7c45d07..764f41966261c 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -286,7 +286,7 @@ export const ESTopHitsAggRT = rt.type({ top_hits: rt.object, }); -interface SnapshotTermsWithAggregation { +export interface SnapshotTermsWithAggregation { terms: { field: string }; aggregations: MetricsUIAggregation; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts index 3664be3b4903a..068c33ea2c31f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { useState } from 'react'; import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -interface MetricThresholdPrefillOptions { +export interface MetricThresholdPrefillOptions { groupBy: string | string[] | undefined; filterQuery: string | undefined; metrics: MetricsExplorerMetric[]; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx index 9c3c89294f403..20e482d9df5b5 100644 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ b/x-pack/plugins/infra/public/components/document_title.tsx @@ -48,19 +48,19 @@ const wrapWithSharedState = () => { return null; } - private getTitle(title: TitleProp) { + public getTitle(title: TitleProp) { return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; } - private pushTitle(title: string) { + public pushTitle(title: string) { titles[this.state.index] = title; } - private removeTitle() { + public removeTitle() { titles.pop(); } - private updateDocumentTitle() { + public updateDocumentTitle() { const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; if (title !== document.title) { document.title = title; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx index a2dd383695983..f1a793d11166c 100644 --- a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -6,13 +6,19 @@ */ import { EuiPanel } from '@elastic/eui'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; - -export const Toolbar = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const Toolbar: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx index 62093dbfe53ec..dfaf0a490225a 100644 --- a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx +++ b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import React from 'react'; - import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import React, { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../src/plugins/kibana_react/common'; -export const FixedDatePicker = euiStyled( +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const FixedDatePicker: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled( ({ className, inputClassName, diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts index 22352b97da0ea..d94e7faa0eabf 100644 --- a/x-pack/plugins/infra/public/components/toolbar_panel.ts +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -5,13 +5,20 @@ * 2.0. */ +import { FunctionComponent } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import { StyledComponent } from 'styled-components'; +import { EuiTheme, euiStyled } from '../../../../../src/plugins/kibana_react/common'; -export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const ToolbarPanel: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx index a94f2dd57c482..b55ae65e58e91 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -6,7 +6,6 @@ */ import { useCallback, useMemo } from 'react'; -import { DatasetFilter } from '../../../common/infra_ml'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; import { useModuleStatus } from './infra_ml_module_status'; @@ -52,7 +51,7 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); @@ -60,7 +59,7 @@ export const useInfraMLModule = ({ { start, end, - datasetFilter, + filter, moduleSourceConfiguration: { indices: selectedIndices, sourceId, @@ -114,13 +113,13 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); cleanUpModule() .then(() => { - setUpModule(selectedIndices, start, end, datasetFilter, partitionField); + setUpModule(selectedIndices, start, end, filter, partitionField); }) .catch(() => { dispatchModuleStatus({ type: 'failedSetup' }); diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts index e681290570b8c..5a5272f783053 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -10,7 +10,6 @@ import { ValidateLogEntryDatasetsResponsePayload, ValidationIndicesResponsePayload, } from '../../../common/http_api/log_analysis'; -import { DatasetFilter } from '../../../common/infra_ml'; import { DeleteJobsResponsePayload } from './api/ml_cleanup'; import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload } from './api/ml_get_module'; @@ -21,7 +20,7 @@ export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api'; export interface SetUpModuleArgs { start?: number | undefined; end?: number | undefined; - datasetFilter?: DatasetFilter; + filter?: any; moduleSourceConfiguration: ModuleSourceConfiguration; partitionField?: string; } diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts index b8d09fdb5e325..a7ab948d052aa 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts @@ -67,6 +67,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'hosts_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // If we have a partition field, we need to change the aggregation to do a terms agg at the top level @@ -126,7 +140,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts index fe92b290dfde3..4c5eb5fd4bf23 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -68,6 +68,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'k8s_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // Because the ML K8s jobs ship with a default partition field of {kubernetes.namespace}, ignore that agg and wrap it in our own agg. @@ -131,7 +145,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 3236cbc59a07b..894f76318bcfe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { debounce } from 'lodash'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import { EuiText, EuiSpacer } from '@elastic/eui'; @@ -22,6 +22,8 @@ import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modul import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor'; +import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery'; interface Props { jobType: 'hosts' | 'kubernetes'; @@ -36,6 +38,8 @@ export const JobSetupScreen = (props: Props) => { const [partitionField, setPartitionField] = useState(null); const h = useMetricHostsModuleContext(); const k = useMetricK8sModuleContext(); + const [filter, setFilter] = useState(''); + const [filterQuery, setFilterQuery] = useState(''); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', @@ -89,7 +93,7 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } else { @@ -97,11 +101,30 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } - }, [cleanUpAndSetUpModule, setUpModule, hasSummaries, indicies, partitionField, startDate]); + }, [ + cleanUpAndSetUpModule, + filterQuery, + setUpModule, + hasSummaries, + indicies, + partitionField, + startDate, + ]); + + const onFilterChange = useCallback( + (f: string) => { + setFilter(f || ''); + setFilterQuery(convertKueryToElasticSearchQuery(f, derivedIndexPattern) || ''); + }, + [derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, 500), [onFilterChange]); const onPartitionFieldChange = useCallback((value: Array<{ label: string }>) => { setPartitionField(value.map((v) => v.label)); @@ -250,6 +273,40 @@ export const JobSetupScreen = (props: Props) => { /> + + + +

+ } + description={ + + } + > + + } + > + + + )} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index 8932388398b6a..acc6ae7af2727 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -27,7 +27,7 @@ const initialState = { type State = Readonly; -export const CustomFieldPanel = class extends React.PureComponent { +export class CustomFieldPanel extends React.PureComponent { public static displayName = 'CustomFieldPanel'; public readonly state: State = initialState; public render() { @@ -86,4 +86,4 @@ export const CustomFieldPanel = class extends React.PureComponent private handleFieldSelection = (selectedOptions: SelectedOption[]) => { this.setState({ selectedOptions }); }; -}; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index c76ff798b1286..d6934c6846b79 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -44,7 +44,7 @@ interface Props { currentTime: number; } -export const Node = class extends React.PureComponent { +export class Node extends React.PureComponent { public readonly state: State = initialState; public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; @@ -164,7 +164,7 @@ export const Node = class extends React.PureComponent { this.setState({ isPopoverOpen: false }); } }; -}; +} const NodeContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 5c57ef11380e5..9f350610b1366 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -39,7 +39,7 @@ const initialState = { type State = Readonly; -export const WaffleGroupByControls = class extends React.PureComponent { +export class WaffleGroupByControls extends React.PureComponent { public static displayName = 'WaffleGroupByControls'; public readonly state: State = initialState; @@ -192,7 +192,7 @@ export const WaffleGroupByControls = class extends React.PureComponent( return [promiseState, execute] as [typeof promiseState, typeof execute]; }; -interface UninitializedPromiseState { +export interface UninitializedPromiseState { state: 'uninitialized'; } -interface PendingPromiseState { +export interface PendingPromiseState { state: 'pending'; promise: Promise; } -interface ResolvedPromiseState { +export interface ResolvedPromiseState { state: 'resolved'; promise: Promise; value: ResolvedValue; } -interface RejectedPromiseState { +export interface RejectedPromiseState { state: 'rejected'; promise: Promise; value: RejectedValue; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e91e085207cb7..99555fa56acd5 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,8 +8,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -79,22 +78,15 @@ export interface InfraPluginSetup { ) => void; } -export class InfraServerPlugin { - private config$: Observable; - public config = {} as InfraConfig; +export class InfraServerPlugin implements Plugin { + public config: InfraConfig; public libs: InfraBackendLibs | undefined; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { - await new Promise((resolve) => { - this.config$.subscribe((configValue) => { - this.config = configValue; - resolve(); - }); - }); + setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json new file mode 100644 index 0000000000000..a8a0e2c7119a9 --- /dev/null +++ b/x-pack/plugins/infra/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 202b80d3d8406..c3e556b167889 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ */ export const PLUGIN_ID = 'lens'; +export const APP_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7e95479887dbd..0d72a366fa411 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,7 +38,7 @@ import { SavedQuery, syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; import { Document } from '../persistence'; @@ -498,7 +498,7 @@ export function App({ isLinkedToOriginatingApp: false, })); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(APP_ID); redirectTo(newInput.savedObjectId); return; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1ff31e5d4bf6b..5869151485a52 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -23,7 +23,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -57,7 +57,7 @@ export async function mountApp( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const historyLocationState = params.history.location.state as HistoryLocationState; - const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const lensServices: LensAppServices = { data, diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 25275ba8e2249..2a6228f16867d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -108,6 +108,40 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); }); + it('should retain width and hidden config from existing state', () => { + const suggestions = datatableVisualization.getSuggestions({ + state: { + layerId: 'first', + columns: [ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + ], + sorting: { + columnId: 'col1', + direction: 'asc', + }, + }, + table: { + isMultiRow: true, + layerId: 'first', + changeType: 'initial', + columns: [numCol('col1'), strCol('col2'), strCol('col3')], + }, + keptLayerIds: [], + }); + + expect(suggestions.length).toBeGreaterThan(0); + expect(suggestions[0].state.columns).toEqual([ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + { columnId: 'col3' }, + ]); + expect(suggestions[0].state.sorting).toEqual({ + columnId: 'col1', + direction: 'asc', + }); + }); + it('should not make suggestions when the table is unchanged', () => { const suggestions = datatableVisualization.getSuggestions({ state: { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 77fda43c37fef..9625a814c7958 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -98,6 +98,12 @@ export const datatableVisualization: Visualization ) { return []; } + const oldColumnSettings: Record = {}; + if (state) { + state.columns.forEach((column) => { + oldColumnSettings[column.columnId] = column; + }); + } const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -126,8 +132,12 @@ export const datatableVisualization: Visualization // table with >= 10 columns will have a score of 0.4, fewer columns reduce score score: (Math.min(table.columns.length, 10) / 10) * 0.4, state: { + ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ columnId: col.columnId })), + columns: table.columns.map((col) => ({ + ...(oldColumnSettings[col.columnId] || {}), + columnId: col.columnId, + })), }, previewIcon: LensIconChartDatatable, // tables are hidden from suggestion bar, but used for drag & drop and chart switching diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 05da76d9fd207..c667ddea06b33 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -40,7 +40,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -182,7 +182,7 @@ export class LensPlugin { }; core.application.register({ - id: 'lens', + id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f79310273..1db463a47dbf0 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -123,7 +123,7 @@ export class LicensingPlugin implements Plugin { private stop$ = new Subject(); private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; private featureUsage = new FeatureUsageService(); @@ -92,13 +91,12 @@ export class LicensingPlugin implements Plugin(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup<{}, LicensingPluginStart>) { + public setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); - const config = await this.config$.pipe(take(1)).toPromise(); - const pollingFrequency = config.api_polling_frequency; + const pollingFrequency = this.config.api_polling_frequency; async function callAsInternalUser( ...args: Parameters @@ -225,7 +223,7 @@ export class LicensingPlugin implements Plugin> => { - return context.config.create().pipe(map((config) => config)); -}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index bc064e236b658..b79d6a0b89a57 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import type { CoreSetup, CoreStart } from 'src/core/server'; @@ -23,7 +22,6 @@ import type { ListsRequestHandlerContext, PluginsStart, } from './types'; -import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; @@ -32,17 +30,17 @@ import { ExceptionListClient } from './services/exception_lists/exception_list_c export class ListPlugin implements Plugin, ListsPluginStart, {}, PluginsStart> { private readonly logger: Logger; + private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; - private config: ConfigType | undefined | null; private security: SecurityPluginStart | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); + this.config = this.initializerContext.config.get(); } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); - this.config = config; + const { config } = this; initSavedObjects(core.savedObjects); diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index ccd30126b67bd..4d1dff9303b0c 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -26,6 +26,7 @@ import { } from '../../../../src/plugins/kibana_utils/public'; import { ListPage, MapPage } from './routes'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { APP_ID } from '../common/constants'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; @@ -80,7 +81,7 @@ export async function renderApp({ function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { const { embeddableId, originatingApp, valueInput } = - stateTransfer.getIncomingEditorState() || {}; + stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; if (routeProps.match.params.savedMapId) { diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 66b65eb8d0a9d..feafb34f6a715 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { getSavedObjectsClient, getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { EmbeddableStateTransfer } from '../../../../../../src/plugins/embeddable/public'; export class LoadListAndRender extends React.Component<{ stateTransfer: EmbeddableStateTransfer }> { @@ -22,7 +22,7 @@ export class LoadListAndRender extends React.Component<{ stateTransfer: Embeddab componentDidMount() { this._isMounted = true; - this.props.stateTransfer.clearEditorState(); + this.props.stateTransfer.clearEditorState(APP_ID); this._loadMapsList(); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index d38ff8b3e4da6..b6ee5274f690d 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { getTimeFilters, @@ -364,7 +364,7 @@ export class SavedMap { this._originatingApp = undefined; // remove editor state so the connection is still broken after reload - this._getStateTransfer().clearEditorState(); + this._getStateTransfer().clearEditorState(APP_ID); getToasts().addSuccess({ title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3116838d26fb5..50c2014275a0f 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -25,7 +25,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; -import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ @@ -53,35 +53,14 @@ export async function getGridTile({ geoFieldType: ES_GEO_FIELD_TYPE; searchSessionId?: string; }): Promise { - const esBbox: ESBounds = tileToESBbox(x, y, z); try { - let bboxFilter; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - bboxFilter = { - geo_bounding_box: { - [geometryFieldName]: esBbox, - }, - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); - bboxFilter = { - geo_shape: { - [geometryFieldName]: { - shape: geojsonPolygon, - relation: 'INTERSECTS', - }, - }, - }; - } else { - throw new Error(`${geoFieldType} is not valid geo field-type`); - } - requestBody.query.bool.filter.push(bboxFilter); - + const tileBounds: ESBounds = tileToESBbox(x, y, z); + requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( z + SUPER_FINE_ZOOM_DELTA, MAX_ZOOM ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; const response = await context .search!.search( @@ -134,14 +113,9 @@ export async function getTile({ }): Promise { let features: Feature[]; try { - requestBody.query.bool.filter.push({ - geo_shape: { - [geometryFieldName]: { - shape: tileToGeoJsonPolygon(x, y, z), - relation: 'INTERSECTS', - }, - }, - }); + requestBody.query.bool.filter.push( + getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) + ); const searchOptions = { sessionId: searchSessionId, @@ -193,7 +167,8 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( - bboxResponse.rawResponse.aggregations.data_bounds.bounds + bboxResponse.rawResponse.aggregations.data_bounds.bounds, + tileToESBbox(x, y, z) ), }, ]; @@ -244,32 +219,31 @@ export async function getTile({ } } -function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { - const wLon = tile2long(x, z); - const sLat = tile2lat(y + 1, z); - const eLon = tile2long(x + 1, z); - const nLat = tile2lat(y, z); - +function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { return { - type: 'Polygon', - coordinates: [ - [ - [wLon, sLat], - [wLon, nLat], - [eLon, nLat], - [eLon, sLat], - [wLon, sLat], - ], - ], + geo_shape: { + [geometryFieldName]: { + shape: { + type: 'envelope', + // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]] + coordinates: [ + [tileBounds.top_left.lon, tileBounds.top_left.lat], + [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat], + ], + }, + relation: 'INTERSECTS', + }, + }, }; } -function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { - let minLon = esBounds.top_left.lon; - const maxLon = esBounds.bottom_right.lon; +function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { + // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. + let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); + const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = esBounds.bottom_right.lat; - const maxLat = esBounds.top_left.lat; + const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); + const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); return { type: 'Polygon', diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b..7440b6ee1e1df 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; // @ts-ignore @@ -134,12 +133,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - async setup(core: CoreSetup, plugins: SetupDeps) { + setup(core: CoreSetup, plugins: SetupDeps) { const { usageCollection, home, licensing, features, mapsLegacy } = plugins; - // @ts-ignore + const mapsLegacyConfig = mapsLegacy.config; const config$ = this._initializerContext.config.create(); - const mapsLegacyConfig = await mapsLegacy.config$.pipe(take(1)).toPromise(); - const currentConfig = await config$.pipe(take(1)).toPromise(); + const currentConfig = this._initializerContext.config.get(); // @ts-ignore const mapsEnabled = currentConfig.enabled; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index 2576e5377b39d..3fffd1588b9b9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -57,14 +57,17 @@ describe('useSwimlaneInputResolver', () => { ), }, anomalyDetectorService: { - getJobs$: jest.fn(() => - of([ + getJobs$: jest.fn((jobId: string[]) => { + if (jobId.includes('invalid-job-id')) { + throw new Error('Invalid job'); + } + return of([ { job_id: 'cw_multi_1', analysis_config: { bucket_span: '15m' }, }, - ]) - ), + ]); + }), }, } as unknown) as AnomalySwimlaneServices, ]; @@ -128,6 +131,31 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3); }); + + test('should not complete the observable on error', async () => { + const { result } = renderHook(() => + useSwimlaneInputResolver( + embeddableInput as Observable, + onInputChange, + refresh, + services, + 1000, + 1 + ) + ); + + await act(async () => { + embeddableInput.next({ + id: 'test-swimlane-embeddable', + jobIds: ['invalid-job-id'], + swimlaneType: SWIMLANE_TYPE.OVERALL, + filters: [], + query: { language: 'kuery', query: '' }, + } as Partial); + }); + + expect(result.current[6]?.message).toBe('Invalid job'); + }); }); describe('processFilters', () => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 5b256b9c5924c..0d75db64a01b9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -47,12 +47,17 @@ const FETCH_RESULTS_DEBOUNCE_MS = 500; function getJobsObservable( embeddableInput: Observable, - anomalyDetectorService: AnomalyDetectorService + anomalyDetectorService: AnomalyDetectorService, + setErrorHandler: (e: Error) => void ) { return embeddableInput.pipe( pluck('jobIds'), distinctUntilChanged(isEqual), - switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)) + switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)), + catchError((e) => { + setErrorHandler(e.body ?? e); + return of(undefined); + }) ); } @@ -95,7 +100,7 @@ export function useSwimlaneInputResolver( useEffect(() => { const subscription = combineLatest([ - getJobsObservable(embeddableInput, anomalyDetectorService), + getJobsObservable(embeddableInput, anomalyDetectorService, setError), embeddableInput, chartWidth$.pipe(skipWhile((v) => !v)), fromPage$, @@ -112,6 +117,11 @@ export function useSwimlaneInputResolver( tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => { + if (!jobs) { + // couldn't load the list of jobs + return of(undefined); + } + const { viewBy, swimlaneType: swimlaneTypeInput, diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a4..b950b064774b1 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -49,7 +49,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 012c050cd3fa8..97e572d15327c 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -7,13 +7,15 @@ import { TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { MonitoringPlugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; export { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export { MonitoringConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); +export { MonitoringPluginSetup, IBulkUploader } from './types'; + +export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 2a5138d0d8880..08224980a558f 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -6,16 +6,9 @@ */ import { coreMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; -import { combineLatest } from 'rxjs'; +import { MonitoringPlugin } from './plugin'; import { AlertsFactory } from './alerts'; -jest.mock('rxjs', () => ({ - // @ts-ignore - ...jest.requireActual('rxjs'), - combineLatest: jest.fn(), -})); - jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, @@ -32,30 +25,11 @@ jest.mock('./kibana_monitoring/collectors', () => ({ registerCollectors: jest.fn(), })); -describe('Monitoring plugin', () => { - const initializerContext = { - logger: { - get: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - })), - }, - config: { - create: jest.fn().mockImplementation(() => ({ - pipe: jest.fn().mockImplementation(() => ({ - toPromise: jest.fn(), - })), - })), - legacy: { - globalConfig$: {}, - }, - }, - env: { - packageInfo: { - version: '1.0.0', - }, - }, - }; +jest.mock('./config', () => ({ + createConfig: (config: any) => config, +})); +describe('Monitoring plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); coreSetup.status.overall$.subscribe = jest.fn(); @@ -71,7 +45,6 @@ describe('Monitoring plugin', () => { }, }; - let config = {}; const defaultConfig = { ui: { elasticsearch: {}, @@ -83,20 +56,7 @@ describe('Monitoring plugin', () => { }, }; - beforeEach(() => { - config = defaultConfig; - (combineLatest as jest.Mock).mockImplementation(() => { - return { - pipe: jest.fn().mockImplementation(() => { - return { - toPromise: jest.fn().mockImplementation(() => { - return [config, 2]; - }), - }; - }), - }; - }); - }); + const initializerContext = coreMock.createPluginInitializerContext(defaultConfig); afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); @@ -104,14 +64,14 @@ describe('Monitoring plugin', () => { }); it('always create the bulk uploader', async () => { - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { const alerts = AlertsFactory.getAll(); - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); expect(setupPlugins.alerts.registerType).toHaveBeenCalledTimes(alerts.length); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6fd9e7534ac65..654c3de7d81a9 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,8 +6,6 @@ */ import Boom from '@hapi/boom'; -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; @@ -21,6 +19,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + Plugin, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -43,6 +42,7 @@ import { AlertsFactory } from './alerts'; import { MonitoringCore, MonitoringLicenseService, + MonitoringPluginSetup, LegacyShimDependencies, IBulkUploader, PluginsSetup, @@ -66,7 +66,8 @@ const wrapError = (error: any): CustomHttpResponseOptions => { }; }; -export class Plugin { +export class MonitoringPlugin + implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; @@ -82,15 +83,9 @@ export class Plugin { this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); } - async setup(core: CoreSetup, plugins: PluginsSetup) { - const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config - .create>() - .pipe(map((rawConfig) => createConfig(rawConfig))), - this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + setup(core: CoreSetup, plugins: PluginsSetup) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); this.legacyShimDependencies = { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0fd30189c5415..bb0b616d37eac 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -94,6 +94,10 @@ export interface IBulkUploader { stop: () => void; } +export interface MonitoringPluginSetup { + getKibanaStats: IBulkUploader['getKibanaStats']; +} + export interface LegacyRequest { logger: Logger; getLogger: (...scopes: string[]) => Logger; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e9a9bb8146dbf..1db5f62823e9b 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,6 +23,7 @@ export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/'; export { useTrackPageview, useUiTracker, + useTrackMetric, UiTracker, TrackMetricOptions, METRIC_TYPE, diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29..a5843d1c4ade1 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,7 +6,6 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -28,10 +27,8 @@ export class ObservabilityPlugin implements Plugin { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: {}): Promise { - const config$ = this.initContext.config.create(); - - const config = await config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup, plugins: {}): ObservabilityPluginSetup { + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts index 19859ab05e6a9..d52f299a692cf 100644 --- a/x-pack/plugins/osquery/server/create_config.ts +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { map } from 'rxjs/operators'; import { PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { ConfigType } from './config'; -export const createConfig$ = ( - context: PluginInitializerContext -): Observable> => { - return context.config.create().pipe(map((config) => config)); +export const createConfig = (context: PluginInitializerContext): Readonly => { + return context.config.get(); }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77509275431e9..c30f4ac057ec0 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -14,7 +13,7 @@ import { Logger, } from '../../../../src/core/server'; -import { createConfig$ } from './create_config'; +import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; @@ -26,9 +25,9 @@ export class OsqueryPlugin implements Plugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('osquery: Setup'); - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return {}; diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts index aefb5429a1b13..996adfdc13f64 100644 --- a/x-pack/plugins/painless_lab/server/plugin.ts +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -22,7 +22,7 @@ export class PainlessLabServerPlugin implements Plugin { this.license = new License(); } - async setup({ http }: CoreSetup, { licensing }: Dependencies) { + setup({ http }: CoreSetup, { licensing }: Dependencies) { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index ba78cf411e369..2b8d9afe979e8 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -29,17 +27,16 @@ export class RemoteClustersServerPlugin implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config$: Observable; + config: ConfigType; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config$ = config.create(); + this.config = config.get(); this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { + setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); const routeDependencies: RouteDependencies = { router, @@ -89,7 +86,7 @@ export class RemoteClustersServerPlugin }); return { - isUiEnabled: config.ui.enabled, + isUiEnabled: this.config.ui.enabled, }; } diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts index 2d8e812408efe..c2a9bfa6506e5 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts @@ -6,6 +6,7 @@ */ import { produce } from 'immer'; +import { Index } from '../../../../types'; const shard1 = { id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_1', '0'], @@ -336,11 +337,14 @@ const search1Child = { (searchRoot.treeRoot as any) = search1; (shard1.searches[0] as any) = searchRoot; -export const processedResponseWithFirstShard = produce(null, () => [ - { - shards: [shard1], - time: 0.058419, - name: '.kibana_1', - visible: false, - }, -]); +export const processedResponseWithFirstShard = produce( + [ + { + shards: [shard1], + time: 0.058419, + name: '.kibana_1', + visible: false, + }, + ], + () => undefined +); diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index cebcbb1a0dc92..ed85febac9a45 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -21,7 +21,7 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { + setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ router, diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 6026e42676c57..66b916ac7f70f 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -15,7 +15,7 @@ import type { import { ConfigSchema } from './config'; import { securityConfigDeprecationProvider } from './config_deprecations'; import { - Plugin, + SecurityPlugin, SecurityPluginSetup, SecurityPluginStart, PluginSetupDependencies, @@ -51,4 +51,4 @@ export const plugin: PluginInitializer< RecursiveReadonly, RecursiveReadonly, PluginSetupDependencies -> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); +> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 84fc410c72cd0..d57951ecb5b1d 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; +import { SecurityPlugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; @@ -16,13 +16,13 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; describe('Security Plugin', () => { - let plugin: Plugin; + let plugin: SecurityPlugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { - plugin = new Plugin( + plugin = new SecurityPlugin( coreMock.createPluginInitializerContext( ConfigSchema.validate({ session: { idleTimeout: 1500 }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 3156af7e930bd..cccfa7de6d177 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,6 +8,7 @@ import { combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { @@ -16,6 +17,7 @@ import { KibanaRequest, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; @@ -101,7 +103,13 @@ export interface PluginStartDependencies { /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class SecurityPlugin + implements + Plugin< + RecursiveReadonly, + RecursiveReadonly, + PluginSetupDependencies + > { private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetup; private auditSetup?: AuditServiceSetup; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 3791c63f662ae..4658e6774b726 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; @@ -38,9 +37,7 @@ export const configSchema = schema.object({ validateArtifactDownloads: schema.boolean({ defaultValue: true }), }); -export const createConfig$ = (context: PluginInitializerContext) => - context.config.create>(); +export const createConfig = (context: PluginInitializerContext) => + context.config.get>(); -export type ConfigType = ReturnType extends Observable - ? T - : ReturnType; +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 6b58ca71f7f4e..a2aff41b68df7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -174,6 +174,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -218,6 +221,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -252,6 +258,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -280,6 +289,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -314,6 +326,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 95563c7c48ef5..3dbaa137bb928 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -54,7 +54,11 @@ export function registerDownloadExceptionListRoute( // The ApiKey must be associated with an enrolled Fleet agent try { scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); - await authenticateAgentWithAccessToken(scopedSOClient, req); + await authenticateAgentWithAccessToken( + scopedSOClient, + context.core.elasticsearch.client.asInternalUser, + req + ); } catch (err) { if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) { return res.unauthorized(); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0d5d83582b42b..8c35fd2ce8f8b 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -6,7 +6,6 @@ */ import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -47,7 +46,7 @@ import { isNotificationAlertExecutor } from './lib/detection_engine/notification import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; -import { createConfig$, ConfigType } from './config'; +import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, @@ -119,8 +118,7 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; - private readonly config$: Observable; - private config?: ConfigType; + private readonly config: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; private setupPlugins?: SetupPlugins; @@ -137,7 +135,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); @@ -146,13 +144,12 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); this.setupPlugins = plugins; - const config = await this.config$.pipe(first()).toPromise(); - this.config = config; - const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.config; + const globalConfig = this.context.config.legacy.get(); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 4a6a1d61a9221..779454e9474ee 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -51,16 +51,8 @@ export const getDataFromSourceHits = ( { category: fieldCategory, field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, + values: toStringArray(item), + originalValue: toStringArray(item), } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf60294..c93b5dbc4c36d 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -43,14 +42,11 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public async setup( + public setup( { http, getStartServices }: CoreSetup, { licensing, features, security, cloud }: Dependencies - ): Promise { - const pluginConfig = await this.context.config - .create() - .pipe(first()) - .toPromise(); + ): void { + const pluginConfig = this.context.config.get(); if (!pluginConfig.enabled) { return; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 9d2a075dc35f9..fb6c00c2f6f48 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; -import { Plugin } from './plugin'; +import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -32,4 +32,4 @@ export const config: PluginConfigDescriptor = { deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index d576858c98e36..d1bf4d51700ba 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsStart } from './plugin'; +import { SpacesPlugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { @@ -20,7 +20,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { @@ -43,7 +43,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -59,7 +59,7 @@ describe('Spaces Plugin', () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing, usageCollection }); @@ -72,7 +72,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -99,7 +99,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(coreSetup, { features, licensing }); const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d9d32dd68c95d..4b26b1016d530 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,6 +13,7 @@ import { CoreStart, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -62,7 +63,8 @@ export interface SpacesPluginStart { spacesService: SpacesServiceStart; } -export class Plugin { +export class SpacesPlugin + implements Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 4cccd82673124..aed115a53fa26 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -124,15 +124,13 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }); if (indexArray.length > 0) { - await refreshEsFields(); + await refreshEsFields(indexArray); } }; - const refreshEsFields = async () => { - if (indexArray.length > 0) { - const currentEsFields = await getFields(http, indexArray); - setEsFields(currentEsFields); - } + const refreshEsFields = async (indices: string[]) => { + const currentEsFields = await getFields(http, indices); + setEsFields(currentEsFields); }; useEffect(() => { @@ -181,7 +179,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< timeField: '', }); } else { - await refreshEsFields(); + await refreshEsFields(indices); } }} onTimeFieldChange={(updatedTimeField: string) => diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb80..1343c46ecdd72 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -19,10 +19,7 @@ export class AlertingBuiltinsPlugin this.logger = ctx.logger.get(); } - public async setup( - core: CoreSetup, - { alerts, features }: StackAlertsDeps - ): Promise { + public setup(core: CoreSetup, { alerts, features }: StackAlertsDeps) { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ @@ -34,6 +31,6 @@ export class AlertingBuiltinsPlugin }); } - public async start(): Promise {} - public async stop(): Promise {} + public start() {} + public stop() {} } diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 77031d4764968..0a879ce92cba6 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -39,7 +39,7 @@ describe('TaskManagerPlugin', () => { pluginInitializerContext.env.instanceUuid = ''; const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); - expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual( + expect(() => taskManagerPlugin.setup(coreMock.createSetup())).toThrow( new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`) ); }); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 62b8b75a38d73..149d111b08f02 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -6,7 +6,7 @@ */ import { combineLatest, Observable, Subject } from 'rxjs'; -import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged } from 'rxjs/operators'; import { PluginInitializerContext, Plugin, @@ -46,7 +46,7 @@ export class TaskManagerPlugin implements Plugin { private taskPollingLifecycle?: TaskPollingLifecycle; private taskManagerId?: string; - private config?: TaskManagerConfig; + private config: TaskManagerConfig; private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); @@ -56,15 +56,11 @@ export class TaskManagerPlugin constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.logger = initContext.logger.get(); + this.config = initContext.config.get(); this.definitions = new TaskTypeDictionary(this.logger); } - public async setup(core: CoreSetup): Promise { - this.config = await this.initContext.config - .create() - .pipe(first()) - .toPromise(); - + public setup(core: CoreSetup): TaskManagerSetupContract { this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); setupSavedObjects(core.savedObjects, this.config); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 2d47740a581b8..ea1bcf82c314c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -96,7 +96,7 @@ describe('jira action params validation', () => { }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); @@ -108,6 +108,23 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.labels': [], + }, + }); + }); + + test('params validation fails when labels contain spaces', () => { + const actionParams = { + subActionParams: { + incident: { summary: 'some title', labels: ['label with spaces'] }, + comments: [], + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.summary': [], + 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 5cb8a76d09bee..26b37278003c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -72,6 +72,7 @@ export function getActionType(): ActionTypeModel => { const errors = { 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.labels': new Array(), }; const validationResult = { errors, @@ -83,6 +84,12 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) + errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + } return validationResult; }, actionParamsFields: lazy(() => import('./jira_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 75930482797a2..cb2d637972cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -184,6 +184,11 @@ const JiraParamsFields: React.FunctionComponent 0 && + incident.labels !== undefined; + return ( <> @@ -304,6 +309,8 @@ const JiraParamsFields: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 3c8bda7792f0a..fe7ea61e68193 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -199,3 +199,10 @@ export const SEARCH_ISSUES_LOADING = i18n.translate( defaultMessage: 'Loading...', } ); + +export const LABELS_WHITE_SPACES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage', + { + defaultMessage: 'Labels cannot contain spaces.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx index b6676cfeed140..ee0f1c4c0ceb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx @@ -34,7 +34,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ inputDisplay: i18n.translate( 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display', { - defaultMessage: 'Only on status change.', + defaultMessage: 'Only on status change', } ), 'data-test-subj': 'onActionGroupChange', diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index be54427b90c57..fbc6691455989 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -91,6 +91,7 @@ export const OfExpression = ({ defaultMessage: 'of', } )} + data-test-subj="ofExpressionPopover" display={display === 'inline' ? 'inline' : 'columns'} value={aggField || firstFieldOption.text} isActive={aggFieldPopoverOpen || !aggField} @@ -119,6 +120,7 @@ export const OfExpression = ({ 0 && aggField !== undefined} error={errors.aggField} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx index cde6980e146b2..d97526d89b62b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx @@ -20,6 +20,7 @@ describe('when expression', () => { { { diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08..3933751105cb4 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -30,7 +30,7 @@ export class TriggersActionsPlugin implements Plugin this.data = getService(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public setup(core: CoreSetup, plugins: PluginsSetup): void { const router = core.http.createRouter(); registerDataService({ logger: this.logger, @@ -42,7 +42,7 @@ export class TriggersActionsPlugin implements Plugin createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } - public async start(): Promise { + public start(): PluginStartContract { return { data: this.data, }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index d77349e53b354..fa6badb34635b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -190,6 +190,7 @@ export class UpgradeAssistantTabs extends React.Component { return [ { id: 'overview', + 'data-test-subj': 'upgradeAssistantOverviewTab', name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', { defaultMessage: 'Overview', }), @@ -197,6 +198,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'cluster', + 'data-test-subj': 'upgradeAssistantClusterTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.clusterTabLabel', { defaultMessage: 'Cluster', }), @@ -213,6 +215,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'indices', + 'data-test-subj': 'upgradeAssistantIndicesTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.indicesTabLabel', { defaultMessage: 'Indices', }), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5aa4a469e4f02..bac67bf722ea7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -6,7 +6,9 @@ exports[`CheckupTab render with deprecations 1`] = ` -

+

-

+

-

+

= ({ <> -

+

= (props) <> - +

{ constructor(_context: PluginInitializerContext) {} - public async setup( - core: CoreSetup, - plugins: ClientPluginsSetup - ): Promise { + public setup(core: CoreSetup, plugins: ClientPluginsSetup): void { if (plugins.home) { plugins.home.featureCatalogue.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 487daf0332a98..a02116877f49a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,10 +5,143 @@ * 2.0. */ -import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; import { NetworkItems, MimeType } from './types'; import { WaterfallDataEntry } from '../../waterfall/types'; +const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + +const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, +]; + +const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -30,139 +163,6 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - const networkItems: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 18098833.175, - requestStartTime: 18098835.439, - loadEndTime: 18098957.145, - timings: { - connect: 81.10800000213203, - wait: 34.577999998873565, - receive: 0.5520000013348181, - send: 0.3600000018195715, - total: 123.97000000055414, - proxy: -1, - blocked: 0.8540000017092098, - queueing: 2.263999998831423, - ssl: 55.38700000033714, - dns: 3.559999997378327, - }, - }, - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - - const networkItemsWithoutFullTimings: NetworkItems = [ - networkItems[0], - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: 2.7929999996558763, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutAnyTimings: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: -1, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutTimingsObject: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - }, - ]; - - const networkItemsWithUncommonMimeType: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/x-javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -175,6 +175,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -188,6 +189,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -201,6 +203,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -214,6 +217,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -227,6 +231,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -240,6 +245,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -253,6 +259,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -266,6 +273,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -279,6 +287,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -292,6 +301,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -305,6 +315,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -316,6 +327,7 @@ describe('getSeriesAndDomain', () => { "y0": 137.70799999925657, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -332,6 +344,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -345,6 +358,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -358,6 +372,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -371,6 +386,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -384,6 +400,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -397,6 +414,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -410,6 +428,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -423,6 +442,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -434,6 +454,7 @@ describe('getSeriesAndDomain', () => { "y0": 0.9219999983906746, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -450,6 +471,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "", + "isHighlighted": true, "showTooltip": false, "tooltipProps": undefined, }, @@ -458,6 +480,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -473,6 +496,7 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { + "isHighlighted": true, "showTooltip": false, }, "x": 0, @@ -480,6 +504,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -501,4 +526,41 @@ describe('getSeriesAndDomain', () => { }); expect(contentDownloadedingConfigItem).toBeDefined(); }); + + it('counts the total number of highlighted items', () => { + // only one CSS file in this array of network Items + const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + expect(actual.totalHighlightedRequests).toBe(1); + }); + + it('adds isHighlighted to waterfall entry when filter matches', () => { + // only one CSS file in this array of network Items + const { series } = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + series.forEach((item) => { + if (item.x === 0) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); + + it('adds isHighlighted to waterfall entry when query matches', () => { + // only the second item matches this query + const { series } = getSeriesAndDomain(networkItems, false, 'director', []); + series.forEach((item) => { + if (item.x === 1) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); +}); + +describe('getSidebarItems', () => { + it('passes the item index offset by 1 to offsetIndex for visual display', () => { + const actual = getSidebarItems(networkItems, false, '', []); + expect(actual[0].offsetIndex).toBe(1); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 0ac93794594c0..46f0d23d0a6b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -55,8 +55,28 @@ const getFriendlyTooltipValue = ({ } return `${label}: ${formatValueForDisplay(value)}ms`; }; +export const isHighlightedItem = ( + item: NetworkItem, + query?: string, + activeFilters: string[] = [] +) => { + if (!query && activeFilters?.length === 0) { + return true; + } + + const matchQuery = query ? item.url?.includes(query) : true; + const matchFilters = + activeFilters.length > 0 ? activeFilters.includes(MimeTypesMap[item.mimeType!]) : true; + + return !!(matchQuery && matchFilters); +}; -export const getSeriesAndDomain = (items: NetworkItems) => { +export const getSeriesAndDomain = ( + items: NetworkItems, + onlyHighlighted = false, + query?: string, + activeFilters?: string[] +) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; @@ -78,13 +98,21 @@ export const getSeriesAndDomain = (items: NetworkItems) => { } }; + let totalHighlightedRequests = 0; + const series = items.reduce((acc, item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + if (isHighlighted) { + totalHighlightedRequests++; + } + if (!item.timings) { acc.push({ x: index, y0: 0, y: 0, config: { + isHighlighted, showTooltip: false, }, }); @@ -96,10 +124,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { let currentOffset = offsetValue - zeroOffset; + let timingValueFound = false; + TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { + timingValueFound = true; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; acc.push({ @@ -108,6 +139,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + isHighlighted, showTooltip: true, tooltipProps: { value: getFriendlyTooltipValue({ @@ -126,7 +158,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { /* if no specific timing values are found, use the total time * if total time is not available use 0, set showTooltip to false, * and omit tooltip props */ - if (!acc.find((entry) => entry.x === index)) { + if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; acc.push({ @@ -134,6 +166,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, config: { + isHighlighted, colour: hasTotal ? mimeTypeColour : '', showTooltip: hasTotal, tooltipProps: hasTotal @@ -154,14 +187,31 @@ export const getSeriesAndDomain = (items: NetworkItems) => { const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; + + let filteredSeries = series; + if (onlyHighlighted) { + filteredSeries = series.filter((item) => item.config.isHighlighted); + } + + return { series: filteredSeries, domain, totalHighlightedRequests }; }; -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { +export const getSidebarItems = ( + items: NetworkItems, + onlyHighlighted: boolean, + query: string, + activeFilters: string[] +): SidebarItems => { + const sideBarItems = items.map((item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method }; + return { url, status, method, isHighlighted, offsetIndex }; }); + if (onlyHighlighted) { + return sideBarItems.filter((item) => item.isHighlighted); + } + return sideBarItems; }; export const getLegendItems = (): LegendItems => { @@ -184,6 +234,7 @@ export const getLegendItems = (): LegendItems => { { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, ]; }); + return [...timingItems, ...mimeTypeItems]; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 8d261edc74bf4..e22caae0d9eb2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -61,16 +61,13 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; -export type CalculatedTimings = { - [K in Timings]?: number; -}; - export enum MimeType { Html = 'html', Script = 'script', Stylesheet = 'stylesheet', Media = 'media', Font = 'font', + XHR = 'xhr', Other = 'other', } @@ -99,6 +96,9 @@ export const FriendlyMimetypeLabels = { [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { defaultMessage: 'Font', }), + [MimeType.XHR]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.xhr', { + defaultMessage: 'XHR', + }), [MimeType.Other]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', { @@ -112,7 +112,6 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, - 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -146,38 +145,18 @@ export const MimeTypesMap: Record = { 'application/font-woff2': MimeType.Font, 'application/vnd.ms-fontobject': MimeType.Font, 'application/font-sfnt': MimeType.Font, + + // XHR + 'application/json': MimeType.XHR, }; export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; -// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. -export interface PayloadTimings { - dns_start: number; - push_end: number; - worker_fetch_start: number; - worker_respond_with_settled: number; - proxy_end: number; - worker_start: number; - worker_ready: number; - send_end: number; - connect_end: number; - connect_start: number; - send_start: number; - proxy_start: number; - push_start: number; - ssl_end: number; - receive_headers_end: number; - ssl_start: number; - request_time: number; - dns_end: number; -} - -export interface ExtraSeriesConfig { - colour: string; -} - -export type SidebarItem = Pick; +export type SidebarItem = Pick & { + isHighlighted: boolean; + offsetIndex: number; +}; export type SidebarItems = SidebarItem[]; export interface LegendItem { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx new file mode 100644 index 0000000000000..e22f4a4c63f59 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -0,0 +1,248 @@ +/* + * Copyright 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 { act, fireEvent } from '@testing-library/react'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import { extractItems, isHighlightedItem } from './data_formatting'; + +import 'jest-canvas-mock'; +import { BAR_HEIGHT } from '../../waterfall/components/constants'; +import { MimeType } from './types'; +import { + FILTER_POPOVER_OPEN_LABEL, + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; + +const getHighLightedItems = (query: string, filters: string[]) => { + return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); +}; + +describe('waterfall chart wrapper', () => { + jest.useFakeTimers(); + + it('renders the correct sidebar items', () => { + const { getAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + }); + + it('search by query works', () => { + const { getAllByTestId, getByTestId, getByLabelText } = render( + + ); + + const filterInput = getByLabelText(FILTER_REQUESTS_LABEL); + + const searchText = '.js'; + + fireEvent.change(filterInput, { target: { value: searchText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems(searchText, []).length; + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + const SIDE_BAR_ITEMS_HEIGHT = NETWORK_EVENTS.events.length * BAR_HEIGHT; + expect(getByTestId('wfSidebarContainer')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + + expect(getByTestId('wfDataOnlyBarChart')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + }); + + it('search by mime type works', () => { + const { getAllByTestId, getByLabelText, getAllByText } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('XHR')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.XHR]).length; + + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + }); + + it('renders sidebar even when filter matches 0 resources', () => { + const { getAllByTestId, getByLabelText, getAllByText, queryAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('CSS')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.Stylesheet]).length; + + // no CSS items found + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + fireEvent.click(getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL)); + + // filter bar is still accessible even when no resources match filter + expect(getByLabelText(FILTER_REQUESTS_LABEL)).toBeInTheDocument(); + + // no resources items are in the chart as none match filter + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); + }); +}); + +const NETWORK_EVENTS = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + requestStartTime: 241114129.214, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + requestStartTime: 241114750.426, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + requestStartTime: 241114270.184, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + requestStartTime: 241114306.416, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + requestStartTime: 241114310.393, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 91657981e7f89..8a0e9729a635b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -5,44 +5,14 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { - WaterfallProvider, - WaterfallChart, - MiddleTruncatedText, - RenderItem, -} from '../../waterfall'; - -export const renderSidebarItem: RenderItem = (item, index) => { - const { status } = item; - - const isErrorStatusCode = (statusCode: number) => { - const is400 = statusCode >= 400 && statusCode <= 499; - const is500 = statusCode >= 500 && statusCode <= 599; - const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; - return is400 || is500 || isSpecific300; - }; - - return ( - <> - {!status || !isErrorStatusCode(status) ? ( - - ) : ( - - - - - - {status} - - - )} - - ); -}; +import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { return {item.name}; @@ -54,23 +24,64 @@ interface Props { } export const WaterfallChartWrapper: React.FC = ({ data, total }) => { + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [networkData] = useState(data); - const { series, domain } = useMemo(() => { - return getSeriesAndDomain(networkData); - }, [networkData]); + const hasFilters = activeFilters.length > 0; + + const { series, domain, totalHighlightedRequests } = useMemo(() => { + return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const sidebarItems = useMemo(() => { - return getSidebarItems(networkData); - }, [networkData]); + return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const legendItems = getLegendItems(); + const renderFilter = useCallback(() => { + return ( + + ); + }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + + const renderSidebarItem: RenderItem = useCallback( + (item) => { + return ( + + ); + }, + [hasFilters, onlyHighlighted] + ); + + useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); + useTrackMetric({ + app: 'uptime', + metric: 'waterfall_chart_view', + metricType: METRIC_TYPE.COUNT, + delay: 15000, + }); + return ( { @@ -81,10 +92,19 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} domain={domain} barStyleAccessor={(datum) => { + if (!datum.datum.config.isHighlighted) { + return { + rect: { + fill: datum.datum.config.colour, + opacity: '0.1', + }, + }; + } return datum.datum.config.colour; }} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx new file mode 100644 index 0000000000000..3acf6a269fb38 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 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, { useState } from 'react'; +import { act, fireEvent } from '@testing-library/react'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { MIME_FILTERS, WaterfallFilter } from './waterfall_filter'; +import { + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, + FILTER_POPOVER_OPEN_LABEL, +} from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + jest.useFakeTimers(); + + it('renders correctly', () => { + const { getByLabelText, getByTitle } = render( + + ); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + MIME_FILTERS.forEach((filter) => { + expect(getByTitle(filter.label)); + }); + }); + + it('filter icon changes color on active/inactive filters', () => { + const Component = () => { + const [activeFilters, setActiveFilters] = useState([]); + + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--primary' + ); + + // toggle it back to inactive + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--text' + ); + }); + + it('search input is working properly', () => { + const setQuery = jest.fn(); + + const Component = () => { + return ( + + ); + }; + const { getByLabelText } = render(); + + const testText = 'js'; + + fireEvent.change(getByLabelText(FILTER_REQUESTS_LABEL), { target: { value: testText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(setQuery).toHaveBeenCalledWith(testText); + }); + + it('resets checkbox when filters are removed', () => { + const Component = () => { + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + const input = getByLabelText(FILTER_REQUESTS_LABEL); + // apply filters + const testText = 'js'; + fireEvent.change(input, { target: { value: testText } }); + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + const filterGroupButton = getByTitle('XHR'); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const collapseCheckbox = getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL) as HTMLInputElement; + expect(collapseCheckbox).not.toBeDisabled(); + fireEvent.click(collapseCheckbox); + expect(collapseCheckbox).toBeChecked(); + + // remove filters + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + // expect the checkbox to reset to disabled and unchecked + expect(collapseCheckbox).not.toBeChecked(); + expect(collapseCheckbox).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx new file mode 100644 index 0000000000000..42c2df4553b4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx @@ -0,0 +1,188 @@ +/* + * Copyright 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, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSpacer, +} from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + FILTER_REQUESTS_LABEL, + FILTER_SCREENREADER_LABEL, + FILTER_REMOVE_SCREENREADER_LABEL, + FILTER_POPOVER_OPEN_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; +import { MimeType, FriendlyMimetypeLabels } from './types'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +interface Props { + query: string; + activeFilters: string[]; + setActiveFilters: Dispatch>; + setQuery: (val: string) => void; + onlyHighlighted: boolean; + setOnlyHighlighted: (val: boolean) => void; +} + +export const MIME_FILTERS = [ + { + label: FriendlyMimetypeLabels[MimeType.XHR], + mimeType: MimeType.XHR, + }, + { + label: FriendlyMimetypeLabels[MimeType.Html], + mimeType: MimeType.Html, + }, + { + label: FriendlyMimetypeLabels[MimeType.Script], + mimeType: MimeType.Script, + }, + { + label: FriendlyMimetypeLabels[MimeType.Stylesheet], + mimeType: MimeType.Stylesheet, + }, + { + label: FriendlyMimetypeLabels[MimeType.Font], + mimeType: MimeType.Font, + }, + { + label: FriendlyMimetypeLabels[MimeType.Media], + mimeType: MimeType.Media, + }, +]; + +export const WaterfallFilter = ({ + query, + setQuery, + activeFilters, + setActiveFilters, + onlyHighlighted, + setOnlyHighlighted, +}: Props) => { + const [value, setValue] = useState(query); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const trackMetric = useUiTracker({ app: 'uptime' }); + + const toggleFilters = (val: string) => { + setActiveFilters((prevState) => + prevState.includes(val) ? prevState.filter((filter) => filter !== val) : [...prevState, val] + ); + }; + useDebounce( + () => { + setQuery(value); + }, + 250, + [value] + ); + + /* reset checkbox when there is no query or active filters + * this prevents the checkbox from being checked in a disabled state */ + useEffect(() => { + if (!(query || activeFilters.length > 0)) { + setOnlyHighlighted(false); + } + }, [activeFilters.length, setOnlyHighlighted, query]); + + // indicates use of the query input box + useEffect(() => { + if (query) { + trackMetric({ metric: 'waterfall_filter_input_changed', metricType: METRIC_TYPE.CLICK }); + } + }, [query, trackMetric]); + + // indicates the collapse to show only highlighted checkbox has been clicked + useEffect(() => { + if (onlyHighlighted) { + trackMetric({ + metric: 'waterfall_filter_collapse_checked', + metricType: METRIC_TYPE.CLICK, + }); + } + }, [onlyHighlighted, trackMetric]); + + // indicates filters have been applied or changed + useEffect(() => { + if (activeFilters.length > 0) { + trackMetric({ + metric: `waterfall_filters_applied_changed`, + metricType: METRIC_TYPE.CLICK, + }); + } + }, [activeFilters, trackMetric]); + + return ( + + + { + setValue(evt.target.value); + }} + value={value} + /> + + + setIsPopoverOpen((prevState) => !prevState)} + color={activeFilters.length > 0 ? 'primary' : 'text'} + isSelected={activeFilters.length > 0} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="rightCenter" + > + + {MIME_FILTERS.map(({ label, mimeType }) => ( + toggleFilters(mimeType)} + key={label} + withNext={true} + aria-label={`${ + activeFilters.includes(mimeType) + ? FILTER_REMOVE_SCREENREADER_LABEL + : FILTER_SCREENREADER_LABEL + } ${label}`} + > + {label} + + ))} + + + 0)} + id="onlyHighlighted" + label={FILTER_COLLAPSE_REQUESTS_LABEL} + checked={onlyHighlighted} + onChange={(e) => { + setOnlyHighlighted(e.target.checked); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx new file mode 100644 index 0000000000000..25b577ef9403a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { SidebarItem } from '../waterfall/types'; +import { MiddleTruncatedText } from '../../waterfall'; +import { SideBarItemHighlighter } from '../../waterfall/components/styles'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +interface SidebarItemProps { + item: SidebarItem; + renderFilterScreenReaderText?: boolean; +} + +export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { + const { status, offsetIndex, isHighlighted } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + const text = `${offsetIndex}. ${item.url}`; + const ariaLabel = `${ + isHighlighted && renderFilterScreenReaderText + ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` + : '' + }${text}`; + + return ( + + {!status || !isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx new file mode 100644 index 0000000000000..578d66a1ea3f1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SidebarItem } from '../waterfall/types'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + const url = 'http://www.elastic.co'; + const offsetIndex = 1; + const item: SidebarItem = { + url, + isHighlighted: true, + offsetIndex, + }; + + it('renders sidbar item', () => { + const { getByText } = render(); + + expect(getByText(`${offsetIndex}. ${url}`)); + }); + + it('render screen reader text when renderFilterScreenReaderText is true', () => { + const { getByLabelText } = render( + + ); + + expect( + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).toBeInTheDocument(); + }); + + it('does not render screen reader text when renderFilterScreenReaderText is false', () => { + const { queryByLabelText } = render( + + ); + + expect( + queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index 543d6004b8955..a4b75174543a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -17,3 +17,5 @@ export const FIXED_AXIS_HEIGHT = 32; // number of items to display in canvas, since canvas can only have limited size export const CANVAS_MAX_ITEMS = 150; + +export const CHART_LEGEND_PADDING = 62; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index 9a3d4efb63a3a..d6c1d777a40a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -25,15 +25,21 @@ describe('getChunks', () => { }); describe('Component', () => { - it('renders truncated text', () => { - const { getByText } = render(); + it('renders truncated text and aria label', () => { + const { getByText, getByLabelText } = render( + + ); expect(getByText(first)).toBeInTheDocument(); expect(getByText(last)).toBeInTheDocument(); + + expect(getByLabelText(longString)).toBeInTheDocument(); }); it('renders screen reader only text', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 9c263312f78f5..ec363ed2b40a4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; +interface Props { + ariaLabel: string; + text: string; +} + const OuterContainer = styled.div` width: 100%; height: 100%; @@ -50,14 +55,14 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ text }: { text: string }) => { +export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( <> - + {text} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index f46bab8c33a85..63b4d2945a51c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -12,7 +12,11 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); @@ -21,9 +25,52 @@ describe('NetworkRequestsTotal', () => { it('message in case total is equal to fetched requests', () => { const { getByText } = render( - + ); expect(getByText('500 network requests')).toBeInTheDocument(); }); + + it('does not show highlighted item message when showHighlightedNetworkEvents is false', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('does not show highlighted item message when highlightedNetworkEvents is less than 0', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('show highlighted item message when highlightedNetworkEvents is greater than 0 and showHighlightedNetworkEvents is true', () => { + const { getByText } = render( + + ); + + expect(getByText(/\(20 match the filter\)/)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx index fce86c6b5c29d..5ccd60b0ce7a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { NetworkRequestsTotalStyle } from './styles'; @@ -13,24 +14,44 @@ import { NetworkRequestsTotalStyle } from './styles'; interface Props { totalNetworkRequests: number; fetchedNetworkRequests: number; + highlightedNetworkRequests: number; + showHighlightedNetworkRequests?: boolean; } -export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { +export const NetworkRequestsTotal = ({ + totalNetworkRequests, + fetchedNetworkRequests, + highlightedNetworkRequests, + showHighlightedNetworkRequests, +}: Props) => { return ( - {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { - defaultMessage: '{numNetworkRequests} network requests', - values: { + fetchedNetworkRequests - ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { - defaultMessage: 'First {count}', - values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, - }) - : totalNetworkRequests, - }, - })} + totalNetworkRequests > fetchedNetworkRequests ? ( + + ) : ( + totalNetworkRequests + ), + }} + />{' '} + {showHighlightedNetworkRequests && highlightedNetworkRequests >= 0 && ( + + )} {totalNetworkRequests > fetchedNetworkRequests && ( = ({ items, render }) => { return ( - + - {items.map((item, index) => { - return ( - - {render(item, index)} - - ); - })} + {items.map((item) => ( + + {render(item)} + + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 333acd6e043df..c00c04b114045 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -14,10 +14,7 @@ interface WaterfallChartOuterContainerProps { height?: string; } -export const WaterfallChartOuterContainer = euiStyled.div` - height: ${(props) => (props.height ? `${props.height}` : 'auto')}; - overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; - overflow-x: hidden; +const StyledScrollDiv = euiStyled.div` &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; @@ -33,11 +30,27 @@ export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled(StyledScrollDiv)` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; - border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; + overflow-y: scroll; + overflow-x: hidden; +`; + +export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` + margin-left: -22px; +`; + +export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` @@ -46,9 +59,18 @@ export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` border: none; `; // NOTE: border-radius !important is here as the "border" prop isn't working +export const WaterfallChartFilterContainer = euiStyled.div` + && { + padding: 16px; + z-index: ${(props) => props.theme.eui.euiZLevel5}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; // NOTE: border-radius !important is here as the "border" prop isn't working + export const WaterfallChartFixedAxisContainer = euiStyled.div` height: ${FIXED_AXIS_HEIGHT}px; z-index: ${(props) => props.theme.eui.euiZLevel4}; + height: 100%; `; interface WaterfallChartSidebarContainer { @@ -74,6 +96,12 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` + opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; + height: 100%; `; interface WaterfallChartChartContainer { @@ -106,6 +134,12 @@ export const WaterfallChartTooltip = euiStyled.div` `; export const NetworkRequestsTotalStyle = euiStyled(EuiText)` - line-height: ${FIXED_AXIS_HEIGHT}px; - margin-left: ${(props) => props.theme.eui.paddingSizes.m} + line-height: 28px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export const RelativeContainer = euiStyled.div` + position: relative; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts new file mode 100644 index 0000000000000..b63ffacaadd2e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts @@ -0,0 +1,50 @@ +/* + * Copyright 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'; + +export const FILTER_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.searchBox.placeholder', + { + defaultMessage: 'Filter network requests', + } +); + +export const FILTER_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.filterScreenreaderLabel', + { + defaultMessage: 'Filter by', + } +); + +export const FILTER_REMOVE_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.removeFilterScreenReaderLabel', + { + defaultMessage: 'Remove filter by', + } +); + +export const FILTER_POPOVER_OPEN_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.popover', + { + defaultMessage: 'Click to open waterfall filters', + } +); + +export const FILTER_COLLAPSE_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.collapseRequestsLabel', + { + defaultMessage: 'Collapse to only show matching requests', + } +); + +export const SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.sidebar.filterMatchesScreenReaderLabel', + { + defaultMessage: 'Resource matches filter', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 1ce46fc0d6e7b..a963fb1e2939c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -10,9 +10,14 @@ import { renderHook } from '@testing-library/react-hooks'; import { IWaterfallContext } from '../context/waterfall_chart'; import { CANVAS_MAX_ITEMS } from './constants'; -const generateTestData = (): IWaterfallContext['data'] => { +const generateTestData = ( + { + xMultiplier, + }: { + xMultiplier: number; + } = { xMultiplier: 1 } +): IWaterfallContext['data'] => { const numberOfItems = 1000; - const data: IWaterfallContext['data'] = []; const testItem = { x: 0, @@ -29,11 +34,11 @@ const generateTestData = (): IWaterfallContext['data'] => { data.push( { ...testItem, - x: i, + x: xMultiplier * i, }, { ...testItem, - x: i, + x: xMultiplier * i, y0: 7, y: 25, } @@ -44,7 +49,7 @@ const generateTestData = (): IWaterfallContext['data'] => { }; describe('useBarChartsHooks', () => { - it('returns result as expected', () => { + it('returns result as expected for non filtered data', () => { const { result, rerender } = renderHook((props) => useBarCharts(props), { initialProps: { data: [] as IWaterfallContext['data'] }, }); @@ -70,4 +75,35 @@ describe('useBarChartsHooks', () => { expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); + + it('returns result as expected for filtered data', () => { + /* multiply x values to simulate filtered data, where x values can have gaps in the + * sequential order */ + const xMultiplier = 2; + const { result, rerender } = renderHook((props) => useBarCharts(props), { + initialProps: { data: [] as IWaterfallContext['data'] }, + }); + + expect(result.current).toHaveLength(0); + const newData = generateTestData({ xMultiplier }); + + rerender({ data: newData }); + + // Thousands items will result in 7 Canvas + expect(result.current.length).toBe(7); + + const firstChartItems = result.current[0]; + const lastChartItems = result.current[4]; + + // first chart items last item should be x 149, since we only display 150 items + expect(firstChartItems[firstChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS - 1) * xMultiplier + ); + + // since here are 5 charts, last chart first item should be x 600 + expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4 * xMultiplier); + expect(lastChartItems[lastChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS * 5 - 1) * xMultiplier + ); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 79fd437039afe..2baf895504911 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -13,27 +13,36 @@ export interface UseBarHookProps { data: IWaterfallContext['data']; } -export const useBarCharts = ({ data = [] }: UseBarHookProps) => { +export const useBarCharts = ({ data }: UseBarHookProps) => { const [charts, setCharts] = useState>([]); useEffect(() => { - if (data.length > 0) { - let chartIndex = 0; - - const chartsN: Array = []; + const chartsN: Array = []; + if (data?.length > 0) { + let chartIndex = 0; + /* We want at most CANVAS_MAX_ITEMS **RESOURCES** per array. + * Resources !== individual timing items, but are comprised of many individual timing + * items. The X value of each item can be used as an id for the resource. + * We must keep track of the number of unique resources added to the each array. */ + const uniqueResources = new Set(); + let lastIndex: number; data.forEach((item) => { - // Subtract 1 to account for x value starting from 0 - if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([item]); + if (uniqueResources.size === CANVAS_MAX_ITEMS && item.x > lastIndex) { chartIndex++; + uniqueResources.clear(); + } + uniqueResources.add(item.x); + lastIndex = item.x; + if (!chartsN[chartIndex]) { + chartsN.push([item]); return; } - chartsN[chartIndex - 1].push(item); + chartsN[chartIndex].push(item); }); - - setCharts(chartsN); } + + setCharts(chartsN); }, [data]); return charts; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 7c9051e8f6acf..528d749f576fc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -6,64 +6,38 @@ */ import React from 'react'; -import { of } from 'rxjs'; -import { MountWithReduxProvider, mountWithRouter } from '../../../../../lib'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { WaterfallChart } from './waterfall_chart'; -import { - renderLegendItem, - renderSidebarItem, -} from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { WaterfallChartOuterContainer } from './styles'; +import { renderLegendItem } from '../../step_detail/waterfall/waterfall_chart_wrapper'; +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; describe('waterfall', () => { it('sets the correct height in case of full height', () => { - const core = mockCore(); - const Component = () => { return ( - `${Number(d).toFixed(0)} ms`} - domain={{ - max: 3371, - min: 0, - }} - barStyleAccessor={(datum) => { - return datum.datum.config.colour; - }} - renderSidebarItem={renderSidebarItem} - renderLegendItem={renderLegendItem} - fullHeight={true} - /> +

+ `${Number(d).toFixed(0)} ms`} + domain={{ + max: 3371, + min: 0, + }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={undefined} + renderLegendItem={renderLegendItem} + fullHeight={true} + /> +
); }; - const component = mountWithRouter( - - - - - - - - ); + const { getByTestId } = render(); - const chartWrapper = component.find(WaterfallChartOuterContainer); + const chartWrapper = getByTestId('waterfallOuterContainer'); - expect(chartWrapper.get(0).props.height).toBe('calc(100vh - 0px)'); + expect(chartWrapper).toHaveStyleRule('height', 'calc(100vh - 62px)'); }); }); - -const mockCore: () => any = () => { - return { - application: { - getUrlForApp: () => '/app/uptime', - navigateToUrl: jest.fn(), - }, - uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), - }, - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx new file mode 100644 index 0000000000000..df00df147fc6c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -0,0 +1,112 @@ +/* + * Copyright 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 { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipInfo, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { BAR_HEIGHT } from './constants'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; +import { useWaterfallContext, WaterfallData } from '..'; + +const getChartHeight = (data: WaterfallData): number => { + // We get the last item x(number of bars) and adds 1 to cater for 0 index + const noOfXBars = new Set(data.map((item) => item.x)).size; + + return noOfXBars * BAR_HEIGHT; +}; + +const Tooltip = (tooltipInfo: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); + }); + return relevantItems.length ? ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ) : null; +}; + +interface Props { + index: number; + chartData: WaterfallData; + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallBarChart = ({ + chartData, + tickFormat, + domain, + barStyleAccessor, + index, +}: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 8f831d0629b25..e0e5165b41e49 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -5,62 +5,30 @@ * 2.0. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - Axis, - BarSeries, - Chart, - Position, - ScaleType, - Settings, - TickFormatter, - DomainRange, - BarStyleAccessor, - TooltipInfo, - TooltipType, -} from '@elastic/charts'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc -// can therefore be accessed. -import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TickFormatter, DomainRange, BarStyleAccessor } from '@elastic/charts'; + import { useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartOuterContainer, WaterfallChartFixedTopContainer, WaterfallChartFixedTopContainerSidebarCover, - WaterfallChartFixedAxisContainer, - WaterfallChartChartContainer, - WaterfallChartTooltip, + WaterfallChartTopContainer, + RelativeContainer, + WaterfallChartFilterContainer, + WaterfallChartAxisOnlyContainer, } from './styles'; -import { WaterfallData } from '../types'; -import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { CHART_LEGEND_PADDING, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { WaterfallBarChart } from './waterfall_bar_chart'; +import { WaterfallChartFixedAxis } from './waterfall_chart_fixed_axis'; import { NetworkRequestsTotal } from './network_requests_total'; -const Tooltip = (tooltipInfo: TooltipInfo) => { - const { data, renderTooltipItem } = useWaterfallContext(); - const relevantItems = data.filter((item) => { - return ( - item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps - ); - }); - return relevantItems.length ? ( - - - {relevantItems.map((item, index) => { - return ( - {renderTooltipItem(item.config.tooltipProps)} - ); - })} - - - ) : null; -}; - -export type RenderItem = (item: I, index: number) => JSX.Element; +export type RenderItem = (item: I, index?: number) => JSX.Element; +export type RenderFilter = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -68,159 +36,100 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; + renderFilter?: RenderFilter; maxHeight?: string; fullHeight?: boolean; } -const getChartHeight = (data: WaterfallData, ind: number): number => { - // We get the last item x(number of bars) and adds 1 to cater for 0 index - return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT; -}; - export const WaterfallChart = ({ tickFormat, domain, barStyleAccessor, renderSidebarItem, renderLegendItem, + renderFilter, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { const { data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, } = useWaterfallContext(); - const [darkMode] = useUiSetting$('theme:darkMode'); - - const theme = useMemo(() => { - return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - }, [darkMode]); - const chartWrapperDivRef = useRef(null); const [height, setHeight] = useState(maxHeight); - const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem); + const shouldRenderSidebar = !!(sidebarItems && renderSidebarItem); const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem); useEffect(() => { if (fullHeight && chartWrapperDivRef.current) { const chartOffset = chartWrapperDivRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${chartOffset}px)`); + setHeight(`calc(100vh - ${chartOffset + CHART_LEGEND_PADDING}px)`); } }, [chartWrapperDivRef, fullHeight]); const chartsToDisplay = useBarCharts({ data }); return ( - - <> - - - {shouldRenderSidebar && ( - - - - - - )} - - - - - - - - - - + + + + {shouldRenderSidebar && ( + + + + {renderFilter && ( + {renderFilter()} + )} - - - + )} + + + + + + + + {shouldRenderSidebar && } - + + {chartsToDisplay.map((chartData, ind) => ( - - - - - - - - - + chartData={chartData} + domain={domain} + barStyleAccessor={barStyleAccessor} + tickFormat={tickFormat} + /> ))} - + - {shouldRenderLegend && } - - + + {shouldRenderLegend && } + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx new file mode 100644 index 0000000000000..3a7ab421b6277 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipType, +} from '@elastic/charts'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartFixedAxisContainer } from './styles'; + +interface Props { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 68d24514a37d3..9e87d69ce38a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -7,12 +7,15 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; +import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; - sidebarItems?: unknown[]; + showOnlyHighlightedNetworkRequests: boolean; + sidebarItems?: SidebarItems; legendItems?: unknown[]; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], @@ -24,8 +27,10 @@ export const WaterfallContext = createContext>({}); interface ProviderProps { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; @@ -34,20 +39,24 @@ interface ProviderProps { export const WaterfallProvider: React.FC = ({ children, data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, renderTooltipItem, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, }) => { return ( diff --git a/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts new file mode 100644 index 0000000000000..f9231abaa75a8 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_chart_theme.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. + */ + +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; + +export const useChartTheme = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + return theme; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx index 9656c63274a13..4c81247fb2cf1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx @@ -8,10 +8,17 @@ import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; import { mountWithIntl, renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { mockState } from '../__mocks__/uptime_store.mock'; +import { KibanaProviderOptions, MockRouter } from './rtl_helpers'; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + state?: Partial; +} const helperWithRouter: ( helper: (node: ReactElement) => R, @@ -67,3 +74,39 @@ export const mountWithRouterRedux = ( options?.storeState ); }; + +/* Custom enzyme render */ +export function render( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return renderWithIntl( + + + {ui} + + + ); +} + +/* Custom enzyme render */ +export function mount( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return mountWithIntl( + + + {ui} + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index abc0451bf8efa..e02a2c6f9832f 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -6,6 +6,7 @@ */ import React, { ReactElement } from 'react'; +import { of } from 'rxjs'; import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -26,7 +27,7 @@ interface KibanaProps { services?: KibanaServices; } -interface KibanaProviderOptions { +export interface KibanaProviderOptions { core?: Partial & ExtraCore; kibanaProps?: KibanaProps; } @@ -54,6 +55,11 @@ const mockCore: () => any = () => { getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), }, + uiSettings: { + get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', + get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + }, + usageCollection: { reportUiCounter: () => {} }, }; return core; diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e75..ceade131fc5af 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -47,7 +47,7 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, diff --git a/x-pack/plugins/xpack_legacy/server/plugin.ts b/x-pack/plugins/xpack_legacy/server/plugin.ts index 9bd42171c75d5..ffef7117bbbd8 100644 --- a/x-pack/plugins/xpack_legacy/server/plugin.ts +++ b/x-pack/plugins/xpack_legacy/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; - import { CoreStart, CoreSetup, @@ -23,11 +21,9 @@ interface SetupPluginDeps { export class XpackLegacyPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { + public setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { const router = core.http.createRouter(); - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initContext.config.legacy.get(); const serverInfo = core.http.getServerInfo(); registerSettingsRoute({ diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts new file mode 100644 index 0000000000000..332a54006b0ec --- /dev/null +++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('Upgrade Assistant Home', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Overview Tab', async () => { + await retry.waitFor('Upgrade Assistant overview tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantOverviewTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Cluster Tab', async () => { + await testSubjects.click('upgradeAssistantClusterTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantClusterTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Indices Tab', async () => { + await testSubjects.click('upgradeAssistantIndicesTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantIndexTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ec1f37ca02be2..d7a9cfc0d08b4 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('uptime', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90555 + describe.skip('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 94a09e3f767f6..24c46c1a1687e 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -31,6 +31,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), + require.resolve('./apps/upgrade_assistant'), ], pageObjects, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 6cc5e2eaefb94..8bd0b8a790d40 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -375,6 +375,34 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.labels]: types that failed validation:\n - [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n - [subActionParams.incident.labels.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + }); + }); + }); }); describe('Execution', () => { 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 cef1bdbba754b..3653d9916466d 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 @@ -34,7 +34,8 @@ export default function ({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // Failing: See https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2705406009062..39b343a361945 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -19,97 +19,97 @@ const EXPECTED_DATA = [ category: 'base', field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: '@version', field: '@version', values: ['1'], - originalValue: '1', + originalValue: ['1'], }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'agent', field: 'agent.type', values: ['filebeat'], - originalValue: 'filebeat', + originalValue: ['filebeat'], }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], - originalValue: '7.0.0', + originalValue: ['7.0.0'], }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], - originalValue: '10.100.7.196', + originalValue: ['10.100.7.196'], }, { category: 'destination', field: 'destination.port', - values: [40684], - originalValue: 40684, + values: ['40684'], + originalValue: ['40684'], }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], - originalValue: '1.0.0-beta2', + originalValue: ['1.0.0-beta2'], }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], - originalValue: 'suricata.eve', + originalValue: ['suricata.eve'], }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: 'event', + originalValue: ['event'], }, { category: 'event', field: 'event.module', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'event', field: 'event.type', values: ['fileinfo'], - originalValue: 'fileinfo', + originalValue: ['fileinfo'], }, { category: 'file', @@ -117,260 +117,261 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'file', field: 'file.size', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: 'eve', + originalValue: ['eve'], }, { category: 'flow', field: 'flow.locality', values: ['public'], - originalValue: 'public', + originalValue: ['public'], }, { category: 'host', field: 'host.architecture', values: ['armv7l'], - originalValue: 'armv7l', + originalValue: ['armv7l'], }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'host', field: 'host.name', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.os.codename', values: ['stretch'], - originalValue: 'stretch', + originalValue: ['stretch'], }, { category: 'host', field: 'host.os.family', values: [''], - originalValue: '', + originalValue: [''], }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + originalValue: ['4.14.50-v7+'], }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + originalValue: ['Raspbian GNU/Linux'], }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], - originalValue: 'raspbian', + originalValue: ['raspbian'], }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', + originalValue: ['9 (stretch)'], }, { category: 'http', field: 'http.request.method', values: ['get'], - originalValue: 'get', + originalValue: ['get'], }, { category: 'http', field: 'http.response.body.bytes', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'http', field: 'http.response.status_code', - values: [206], - originalValue: 206, + values: ['206'], + originalValue: ['206'], }, { category: 'input', field: 'input.type', values: ['log'], - originalValue: 'log', + originalValue: ['log'], }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + originalValue: ['/var/log/suricata/eve.json'], }, { category: 'log', field: 'log.offset', - values: [1856288115], - originalValue: 1856288115, + values: ['1856288115'], + originalValue: ['1856288115'], }, { category: 'network', field: 'network.name', values: ['iot'], - originalValue: 'iot', + originalValue: ['iot'], }, { category: 'network', field: 'network.protocol', values: ['http'], - originalValue: 'http', + originalValue: ['http'], }, { category: 'network', field: 'network.transport', values: ['tcp'], - originalValue: 'tcp', + originalValue: ['tcp'], }, { category: 'service', field: 'service.type', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'source', field: 'source.as.num', - values: [16509], - originalValue: 16509, + values: ['16509'], + originalValue: ['16509'], }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + originalValue: ['Amazon.com, Inc.'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], - originalValue: 'Seattle', + originalValue: ['Seattle'], }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], - originalValue: 'North America', + originalValue: ['North America'], }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: 'US', + originalValue: ['US'], }, { category: 'source', field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + values: ['47.6103'], + originalValue: ['47.6103'], }, { category: 'source', field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + values: ['-122.3341'], + originalValue: ['-122.3341'], }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['US-WA'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', + originalValue: ['Washington'], }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], - originalValue: '54.239.219.210', + originalValue: ['54.239.219.210'], }, { category: 'source', field: 'source.port', - values: [80], - originalValue: 80, + values: ['80'], + originalValue: ['80'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], - originalValue: 'CLOSED', + originalValue: ['CLOSED'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', - values: [301], - originalValue: 301, + values: ['301'], + originalValue: ['301'], }, { category: 'suricata', field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + values: ['196625917175466'], + originalValue: ['196625917175466'], }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['video/mp4'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], - originalValue: 'eth0', + originalValue: ['eth0'], }, { category: 'base', @@ -382,7 +383,7 @@ const EXPECTED_DATA = [ category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'url', @@ -390,8 +391,9 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -399,26 +401,27 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], }, { category: '_score', field: '_score', - values: [1], - originalValue: 1, + values: ['1'], + originalValue: ['1'], }, ]; @@ -452,7 +455,6 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 9f016ab044a90..2ba83bff6f1b1 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -38,9 +38,8 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(false); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(false); }); it('sets given is_managed value', async () => { @@ -56,9 +55,25 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(true); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST3', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 400 with an empty namespace', async () => { @@ -242,6 +257,23 @@ export default function ({ getService }: FtrProviderContext) { const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); const json = getRes.body; expect(json.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .put(`/api/fleet/agent_policies/${agentPolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST2', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 409 if policy already exists with name given', async () => { @@ -276,5 +308,54 @@ export default function ({ getService }: FtrProviderContext) { expect(body.message).to.match(/already exists?/); }); }); + + describe('POST /api/fleet/agent_policies/delete', () => { + let managedPolicy: any | undefined; + it('should prevent managed policies being deleted', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Managed policy', + namespace: 'default', + is_managed: true, + }) + .expect(200); + managedPolicy = createdPolicy; + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: managedPolicy.id }) + .expect(400); + + expect(body.message).to.contain('Cannot delete managed policy'); + }); + + it('should allow unmanaged policies being deleted', async () => { + const { + body: { item: unmanagedPolicy }, + } = await supertest + .put(`/api/fleet/agent_policies/${managedPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Unmanaged policy', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: unmanagedPolicy.id }); + + expect(body).to.eql({ + id: unmanagedPolicy.id, + name: 'Unmanaged policy', + }); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 23b7464a317e9..0020e6bdf1bb0 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -11,7 +11,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); - //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_by_upload')); loadTestFile(require.resolve('./install_overrides')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/template.ts b/x-pack/test/fleet_api_integration/apis/epm/template.ts index c7e9e21155257..d79452ca0eb6f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/template.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/template.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex import { getTemplate } from '../../../../plugins/fleet/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { - const indexPattern = 'foo'; const templateName = 'bar'; + const templateIndexPattern = 'bar-*'; const es = getService('es'); const mappings = { properties: { @@ -25,27 +25,36 @@ export default function ({ getService }: FtrProviderContext) { it('can be loaded', async () => { const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, mappings, packageName: 'system', composedOfTemplates: [], + templatePriority: 200, }); // This test is not an API integration test with Kibana // We want to test here if the template is valid and for this we need a running ES instance. // If the ES instance takes the template, we assume it is a valid template. - const { body: response1 } = await es.indices.putTemplate({ - name: templateName, + const { body: response1 } = await es.transport.request({ + method: 'PUT', + path: `/_index_template/${templateName}`, body: template, }); + // Checks if template loading worked as expected expect(response1).to.eql({ acknowledged: true }); - const { body: response2 } = await es.indices.getTemplate({ name: templateName }); + const { body: response2 } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + }); + // Checks if the content of the template that was loaded is as expected // We already know based on the above test that the template was valid // but we check here also if we wrote the index pattern inside the template as expected - expect(response2[templateName].index_patterns).to.eql([`${indexPattern}-*`]); + expect(response2.index_templates[0].index_template.index_patterns).to.eql([ + templateIndexPattern, + ]); }); }); } diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 55474bd5a7688..5b2632ef710e4 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); const log = getService('log'); const pieChart = getService('pieChart'); const find = getService('find'); @@ -30,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('sample data dashboard', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 738e45c1cbcf1..5cbd5dff45e1e 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -156,5 +156,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); + + it('unlink lens panel from embeddable library', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + }); + + it('save lens panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel); + await testSubjects.click('confirmSaveSavedObjectButton'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.existsByLinkText('lnsPieVis'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js new file mode 100644 index 0000000000000..40e73f0d8a763 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + describe('maps in embeddable library', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.clickSaveAndReturnButton(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('save map panel to embeddable library', async () => { + await dashboardPanelActions.saveToLibrary('embeddable library map'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const mapPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + mapPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + await dashboardPanelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('embeddable library map'); + await find.existsByLinkText('embeddable library map'); + await dashboardAddPanel.closeAddPanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 815de2e081309..9fd4c9db703db 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -9,6 +9,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d76afb7ebdc24..dd20ed58afbc6 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mapbox_styles')); loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 094f5335cd05f..49717016f9c60 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; -import { MAPBOX_STYLES } from './mapbox_styles'; - const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, @@ -18,10 +16,6 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; -const CIRCLE_STYLE_LAYER_INDEX = 0; -const FILL_STYLE_LAYER_INDEX = 2; -const LINE_STYLE_LAYER_INDEX = 3; -const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -95,34 +89,6 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points, lines, and bounding-boxes independently', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { - return mbLayer.id.startsWith(VECTOR_SOURCE_ID); - }); - - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); - - //fill layer - expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); - - //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); - - //Too many features layer (this is a static style config) - expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: ['==', ['get', '__kbn_too_many_features__'], true], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); - it('should flag only the joined features as visible', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d4496f13b8bef..b483b95e0ca1f 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -5,176 +5,242 @@ * 2.0. */ -export const MAPBOX_STYLES = { - POINT_LAYER: { - id: 'n1t6f_circle', - type: 'circle', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'circle-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mapbox styles', () => { + let mapboxStyle; + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'geoshape_data_reader', 'meta_for_geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('join example'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should style circle layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_circle'; + }); + expect(layer).to.eql({ + id: 'n1t6f_circle', + type: 'circle', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', - [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, - ], - 2, + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'circle-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'circle-opacity': 0.75, - 'circle-stroke-color': '#41937c', - 'circle-stroke-opacity': 0.75, - 'circle-stroke-width': 1, - 'circle-radius': 10, - }, - }, - FILL_LAYER: { - id: 'n1t6f_fill', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, + 'circle-stroke-width': 1, + 'circle-radius': 10, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_fill'; + }); + expect(layer).to.eql({ + id: 'n1t6f_fill', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], ], - 2, + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, + ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', + ], + 'fill-opacity': 0.75, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_line'; + }); + expect(layer).to.eql({ + id: 'n1t6f_line', + type: 'line', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], ], ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'fill-opacity': 0.75, - }, - }, - LINE_LAYER: { - id: 'n1t6f_line', - type: 'line', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], ], - ], - ], - layout: { visibility: 'visible' }, - paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, - }, -}; + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, + }); + }); + + it('should style incomplete data layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_toomanyfeatures'; + }); + expect(layer).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts index 49728603c246c..b8bdc7de16e1e 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('total feature importance panel and decision path popover', function () { + // Failing: See https://github.com/elastic/kibana/issues/90526 + describe.skip('total feature importance panel and decision path popover', function () { const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index bb60de86aef82..95ebc7b2ff5d5 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -70,7 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -81,10 +80,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('can view all saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, @@ -189,7 +184,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -200,10 +194,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('cannot view any saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json index f085bad4c507e..b63ae2295f70b 100644 --- a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json @@ -66,20 +66,3 @@ } } } - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index fe29bad0fa381..26b033e28b4da 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -125,26 +125,8 @@ { "type": "doc", "value": { - "id": "custom-space:index-pattern:metricbeat-*", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fieldFormatMap": "{\"aerospike.namespace.device.available.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.device.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.memory.used.data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.index.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.sindex.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"aws.rds.disk_usage.bin_log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_local_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.freeable_memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.latency.commit\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.ddl\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.dml\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.insert\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.read\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.select\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.update\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.write\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.replica_lag.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.swap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.volume_used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_daily_storage.bucket.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.downloaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.latency.first_byte.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.latency.total_request.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.requests.select_returned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.requests.select_scanned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.uploaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.sqs.oldest_message_age.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.sqs.sent_message_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.degraded.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.misplace.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.pg.avail_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.data_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.total_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.used_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.read_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.write_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.misc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.sst.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.total.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.pct\":{\"id\":\"percent\",\"params\":{}},\"ceph.pool_disk.stats.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.pool_disk.stats.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.nat.port\":{\"id\":\"string\",\"params\":{}},\"client.port\":{\"id\":\"string\",\"params\":{}},\"coredns.stats.dns.request.duration.ns.sum\":{\"id\":\"duration\",\"params\":{}},\"couchbase.bucket.data.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.ram.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.use.pct\":{\"id\":\"percent\",\"params\":{}},\"couchbase.cluster.hdd.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.quota.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.disk_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.mcd_memory.allocated.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.nat.port\":{\"id\":\"string\",\"params\":{}},\"destination.port\":{\"id\":\"string\",\"params\":{}},\"docker.cpu.core.*.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.kernel.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.summary.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.peak\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.limit\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.private_working_set.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.rss.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.max\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.usage.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.inbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.outbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.indices.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.disk.mvcc_db_total_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.memory.go_memstats_alloc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_received.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_sent.bytes\":{\"id\":\"bytes\",\"params\":{}},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\",\"params\":{}},\"event.severity\":{\"id\":\"string\",\"params\":{}},\"golang.heap.allocations.active\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.allocated\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.idle\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.total\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.gc.next_gc_limit\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.obtained\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.released\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.stack\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.total\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.info.memory.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.ssl.frontend.session_reuse.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.stat.compressor.bypassed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.throttle.pct\":{\"id\":\"percent\",\"params\":{}},\"http.request.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.status_code\":{\"id\":\"string\",\"params\":{}},\"kibana.stats.process.memory.heap.size_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.logs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.allocatable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.working_set.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.avg_obj_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.extent_free_list.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.index_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.storage_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.headroom.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.headroom.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.oplog.size.allocated\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.oplog.size.used\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.extra_info.heap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.dirty.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.maximum.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.max_file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.received\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.sent\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.cpu\":{\"id\":\"percent\",\"params\":{}},\"nats.stats.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.mem.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.uptime\":{\"id\":\"duration\",\"params\":{}},\"nats.subscriptions.cache.hit_rate\":{\"id\":\"percent\",\"params\":{}},\"network.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"process.pgid\":{\"id\":\"string\",\"params\":{}},\"process.pid\":{\"id\":\"string\",\"params\":{}},\"process.ppid\":{\"id\":\"string\",\"params\":{}},\"process.thread.id\":{\"id\":\"string\",\"params\":{}},\"rabbitmq.connection.frame_max\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.gc.reclaimed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.queue.consumers.utilisation.pct\":{\"id\":\"percent\",\"params\":{}},\"rabbitmq.queue.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.active\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.allocated\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.resident\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.max.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.dataset\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.lua\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.peak\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.rss\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.rewrite.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.size.base\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.size.current\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.backlog.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.master.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.left_bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.nat.port\":{\"id\":\"string\",\"params\":{}},\"server.port\":{\"id\":\"string\",\"params\":{}},\"source.bytes\":{\"id\":\"bytes\",\"params\":{}},\"source.nat.port\":{\"id\":\"string\",\"params\":{}},\"source.port\":{\"id\":\"string\",\"params\":{}},\"system.core.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.diskio.iostat.read.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.iostat.write.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.entropy.pct\":{\"id\":\"percent\",\"params\":{}},\"system.filesystem.available\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.free\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.total\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.fsstat.total_size.free\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.total\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.used\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.default_size\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.free\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.reserved\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.surplus\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.total\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.swap.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.blkio.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.cache.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memory_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memsw_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.mapped_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss_huge.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.swap.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.unevictable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.share\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.size\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.tcp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.udp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.uptime.duration.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}},\"url.port\":{\"id\":\"string\",\"params\":{}},\"vsphere.datastore.capacity.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.pct\":{\"id\":\"percent\",\"params\":{}},\"vsphere.host.memory.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.free.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.total.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.host.bytes\":{\"id\":\"bytes\",\"params\":{}},\"windows.service.uptime.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}}}", - "timeFieldName": "@timestamp", - "title": "metricbeat-*" - }, - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "type": "index-pattern", - "updated_at": "2020-01-22T15:34:59.061Z" - } - } -} - -{ - "type": "doc", - "value": { + "index": ".kibana", + "type": "doc", "id": "index-pattern:logstash-*", "index": ".kibana_1", "source": { @@ -297,4 +279,4 @@ "updated_at": "2019-07-17T17:54:26.378Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index da1518ed72b48..1c4a85450a8da 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -24,7 +24,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr return await retry.try(async () => { await common.navigateToApp('settings'); await testSubjects.click('upgrade_assistant'); - retry.waitFor('url to contain /upgrade_assistant', async () => { + await retry.waitFor('url to contain /upgrade_assistant', async () => { const url = await browser.getCurrentUrl(); return url.includes('/upgrade_assistant'); }); @@ -61,7 +61,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr async waitForTelemetryHidden() { const self = this; - retry.waitFor('Telemetry to disappear.', async () => { + await retry.waitFor('Telemetry to disappear.', async () => { return (await self.isTelemetryExists()) === false; }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index d38ad278d3f64..6a051cc9fc5e6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); + const comboBox = getService('comboBox'); async function getAlertsByName(name: string) { const { @@ -30,15 +31,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function defineAlert(alertName: string, alertType?: string) { - alertType = alertType || '.index-threshold'; + async function defineEsQueryAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click(`${alertType}-SelectOption`); + await testSubjects.click(`.es-query-SelectOption`); await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); await filterSelectItem.click(); await testSubjects.click('thresholdAlertTimeFieldSelect'); @@ -53,6 +53,44 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); } + async function defineIndexThresholdAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click(`.index-threshold-SelectOption`); + await testSubjects.click('selectIndexExpression'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + + await testSubjects.click('whenExpression'); + await testSubjects.click('whenExpressionSelect'); + await retry.try(async () => { + const aggTypeOptions = await find.allByCssSelector('#aggTypeField option'); + expect(aggTypeOptions[1]).not.to.be(undefined); + await aggTypeOptions[1].click(); + }); + + await testSubjects.click('ofExpressionPopover'); + const ofComboBox = await find.byCssSelector('#ofField'); + await ofComboBox.click(); + const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox'); + const ofOptions = ofOptionsString.trim().split('\n'); + expect(ofOptions.length > 0).to.be(true); + await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]); + } + async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); @@ -67,7 +105,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName); + await defineIndexThresholdAlert(alertName); await testSubjects.click('notifyWhenSelect'); await testSubjects.click('onThrottleInterval'); @@ -222,7 +260,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should successfully test valid es_query alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName, '.es-query'); + await defineEsQueryAlert(alertName); // Valid query await testSubjects.setValue('queryJsonEditor', '{"query":{"match_all":{}}}', { diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 5946a502a4ce3..59d6074d9d8ca 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -33,7 +33,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...apiConfig.get('kbnTestServer'), serverArgs: [ - `--migrations.enableV2=false`, `--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`, `--logging.json=false`, `--server.maxPayloadBytes=1679958`, diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts index 415cdf4814176..ed4bc8f4f8c7b 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('DELETE /api/saved_objects_tagging/tags/{id}', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90552 + describe.skip('DELETE /api/saved_objects_tagging/tags/{id}', () => { beforeEach(async () => { await esArchiver.load('delete_with_references'); }); diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index 69b3e05946345..bf79d35178a60 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -47,9 +47,7 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { public async disabledOrFail() { await this.exists(); - await expect(await (await (await this.find()).findByTagName('button')).isEnabled()).to.be( - false - ); + await expect(await (await this.find()).getAttribute('data-save-disabled')).to.be('true'); } public async expectState(state: SessionStateType) { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 10943b3a2929f..0a7a30f373e07 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,73 +9,73 @@ "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/charts/tsconfig.json" }, { "path": "../../src/plugins/console/tsconfig.json" }, { "path": "../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, { "path": "../../src/plugins/home/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, { "path": "../../src/plugins/kibana_overview/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/legacy_export/tsconfig.json" }, + { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, - + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, { "path": "../plugins/alerts/tsconfig.json" }, + { "path": "../plugins/beats_management/tsconfig.json" }, + { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, - { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, - { "path": "../plugins/enterprise_search/tsconfig.json" }, - { "path": "../plugins/global_search/tsconfig.json" }, - { "path": "../plugins/global_search_providers/tsconfig.json" }, - { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/event_log/tsconfig.json" }, - { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/global_search_bar/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/grokdebugger/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/infra/tsconfig.json" }, + { "path": "../plugins/ingest_pipelines/tsconfig.json" }, { "path": "../plugins/lens/tsconfig.json" }, + { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, + { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/painless_lab/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "../plugins/security/tsconfig.json" }, + { "path": "../plugins/snapshot_restore/tsconfig.json" }, + { "path": "../plugins/spaces/tsconfig.json" }, + { "path": "../plugins/stack_alerts/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/transform/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "../plugins/spaces/tsconfig.json" }, - { "path": "../plugins/security/tsconfig.json" }, - { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "../plugins/stack_alerts/tsconfig.json" }, - { "path": "../plugins/beats_management/tsconfig.json" }, - { "path": "../plugins/cloud/tsconfig.json" }, - { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "../plugins/global_search_bar/tsconfig.json" }, - { "path": "../plugins/observability/tsconfig.json" }, - { "path": "../plugins/ingest_pipelines/tsconfig.json" }, - { "path": "../plugins/license_management/tsconfig.json" }, - { "path": "../plugins/snapshot_restore/tsconfig.json" }, - { "path": "../plugins/grokdebugger/tsconfig.json" }, - { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" }, - { "path": "../plugins/runtime_fields/tsconfig.json" }, - { "path": "../plugins/index_management/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 6fabd16752dfa..5d51c2923abd0 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,6 +22,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", "plugins/enterprise_search/**/*", + "plugins/infra/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", @@ -118,6 +119,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index e35cfe4e024a2..ae88ab6486e64 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -23,6 +23,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 24fe6463fa41c..ec6cf338a43da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5968,6 +5968,11 @@ resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" integrity sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/json5@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" @@ -6798,7 +6803,7 @@ dependencies: "@types/webpack" "*" -"@types/webpack-sources@*": +"@types/webpack-sources@*", "@types/webpack-sources@^0.1.4": version "0.1.5" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== @@ -13614,6 +13619,14 @@ eslint-import-resolver-node@0.3.2, eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + eslint-import-resolver-webpack@0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.11.1.tgz#fcf1fd57a775f51e18f442915f85dd6ba45d2f26" @@ -13638,6 +13651,14 @@ eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + eslint-plugin-babel@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" @@ -13693,6 +13714,25 @@ eslint-plugin-import@^2.19.1: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + eslint-plugin-jest@^24.0.2: version "24.0.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.0.2.tgz#4bf0fcdc86289d702a7dacb430b4363482af773b" @@ -16960,11 +17000,16 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@1.10.0, immer@^1.5.0: +immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" + integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -19382,13 +19427,6 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -leaflet-vega@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.8.6.tgz#dd4090a6123cb983c2b732d53ec9e4daa53736b2" - integrity sha1-3UCQphI8uYPCtzLVPsnk2qU3NrI= - dependencies: - vega-spec-injector "^0.0.2" - leaflet.heat@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/leaflet.heat/-/leaflet.heat-0.2.0.tgz#109d8cf586f0adee41f05aff031e27a77fecc229" @@ -25534,7 +25572,7 @@ resolve@1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: +resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -28427,6 +28465,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tsd@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.13.1.tgz#d2a8baa80b8319dafea37fbeb29fef3cec86e92b" @@ -29519,7 +29567,7 @@ vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== -vega-expression@^4.0.0, vega-expression@^4.0.1, vega-expression@~4.0.1: +vega-expression@^4.0.1, vega-expression@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== @@ -29553,24 +29601,7 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.10.0.tgz#3d384111f13b3b0dd38a4fca656c5ae54b66e158" - integrity sha512-1l28OxUwOj8FEvRU62Oz2hiTuDECrvx1DPU1qLebBKhlgaKbcCk3XyHrn1kUzhMKpXq+SFv5VPxchZP47ASSvQ== - dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" - vega-dataflow "^5.7.3" - vega-expression "^4.0.1" - vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.1.5" - vega-statistics "^1.7.9" - vega-time "^2.0.4" - vega-util "^1.16.0" - -vega-functions@^5.12.0, vega-functions@~5.12.0: +vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== @@ -29697,19 +29728,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" - integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== - dependencies: - d3-path "^2.0.0" - d3-shape "^2.0.0" - vega-canvas "^1.2.5" - vega-loader "^4.3.3" - vega-scale "^7.1.1" - vega-util "^1.15.2" - -vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: +vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== @@ -29726,14 +29745,6 @@ vega-schema-url-parser@^2.1.0: resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== -vega-selections@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.5.tgz#c7662edf26c1cfb18623573b30590c9774348d1c" - integrity sha512-oRSsfkqYqA5xfEJqDpgnSDd+w0k6p6SGYisMD6rGXMxuPl0x0Uy6RvDr4nbEtB+dpWdoWEvgrsZVS6axyDNWvQ== - dependencies: - vega-expression "^4.0.0" - vega-util "^1.15.2" - vega-selections@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7"