diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 025836a90204c5..a89ff166bf32e9 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/.github/CODEOWNERS b/.github/CODEOWNERS index 3884f975c813d5..2917cc52a6c6db 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,6 +150,12 @@ /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @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 diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a78..92a624649d3c50 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 00000000000000..cf315e1fd337e3 --- /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 00000000000000..54507b44cdd72a --- /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 00000000000000..f16d3c46bf8499 --- /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 00000000000000..f809f75783c26c --- /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 efd499823ffadc..e307b5c9971b0b 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 7fa05588a33012..232851cd342cee 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 0d3c19a8217a67..ec5ed211a9d2ba 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 1fcc2999dfd2ed..b7c3e11e492bd7 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 00000000000000..1ad1d87220b748 --- /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 00000000000000..1d033b7b88b051 --- /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 00000000000000..3cce90f01603bb --- /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 00000000000000..9272fc2c4eba06 --- /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 82f4a285409c95..5fe5eda7a81729 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 b4e6623098736d..a8b0aae28d251b 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 03e889a018b6f0..851f84474fe11f 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 839eabff29a189..fe55e131065ddd 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 00000000000000..2b897db7bba4c3 --- /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 00000000000000..c7e10fc42ead19 --- /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 00000000000000..4b4ede2f77a7e2 --- /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 9b69012ed5f123..2dc78f2df3a833 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/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 83e7edc5a016a6..d7a9373a6e2a99 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 f79885e3bc7163..d9073ecca1145f 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 445a5202ffd0c2..44fcf1a2600b8e 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/package.json b/package.json index 139183c4a11def..7144745f2ae358 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", @@ -715,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", @@ -834,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", 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 d3494512d055a4..f86865ffa66704 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 70dc5370c5a269..b00b9fb8b3f25f 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 6d48c0b2bbaead..64e5626c94c8bb 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-pm/dist/index.js b/packages/kbn-pm/dist/index.js index abb941d2117131..d939e7b3000fa3 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48119,6 +48119,29 @@ async function isBazeliskInstalled(bazeliskVersion) { } } +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'); @@ -48128,7 +48151,9 @@ async function installBazelTools(repoRootPath) { const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed + 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`); diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index cee6eff317afa4..b547c2bc141bda 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -52,6 +52,29 @@ async function isBazeliskInstalled(bazeliskVersion: string) { } } +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'); @@ -66,6 +89,9 @@ export async function installBazelTools(repoRootPath: string) { // 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.info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19a..d79594c97cec78 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 61197a2a8bf70a..f7c119acd0c7a4 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 ce4e50bf9b2ac7..9d8f7703c026dc 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_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc1..02c55b6af91dcc 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/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f2..a1cb036ce38f8f 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 d208ea76c48fe3..e47de84ea12b2f 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 76811d4908d22a..be805c6a521ce8 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 e8e930a5befca6..ef919018f120b3 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 af95e831a64721..a08a6cf0b431a0 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 d44657f9039a35..1f85482569dbc2 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 a22d48c50247a4..e70b78f237d757 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 7a10ce1cdfc772..57fbe4cbecd12f 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 75ed9aa5f150f6..99579ada8ec588 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 382a694bd2e418..6f478004c204ef 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 dda947972737a6..a29fb01fbc0092 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 68fdfdf62c30b0..c90d2e804225c7 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 83b3fb53689a75..ca7f11e28de75f 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 1b5994c40c0410..5c38deeb5cf6ec 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 1b5e3bbb06e71d..b7b8c297ea5717 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 91ccc2dedf272c..45db98201b7587 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 fe2ce76446cb99..b22c326061f662 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 1bb4e57b0ac299..46cfd935f429b3 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/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 895db80983fc17..5531f847f8bb41 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 c9a3aa25db4c15..6f915df9dd9588 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 7574f26979ab1e..344a0d151cfb9e 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 2484daf2ea8754..3838e4d3b3c8e7 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 1a717f330d4c25..de47ab9c596114 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 db68b2f87d5772..2fa7acfb6cab62 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 dbbb0faf35c31f..609ce2692c7770 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 76e422d24732e4..fa5517303f18f2 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 b9ad6ce15df2be..6ba23747cf3745 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 121cb82155b6ec..f28822d95d8142 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 81220f897f36ba..e84c638d3ec999 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 19c6e3d99d6c21..404074124c92b7 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 682b583f6a791f..2a664328d4df29 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 857973c5ae0068..cb605dac567772 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 a24a4a1b51f6a5..623d2dcc71faca 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 fc784ac80ed8d2..e933badfe80feb 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 717fd5fc5ab929..32f12193306e72 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 59a9210ff51306..e1aa1ab2f956da 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 2495679a2f8c2d..581145c7c09d1b 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 0a1c18c01ad82d..aac508fb5b909c 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 a662a374b063ef..fcd72aa4326a20 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 40a12290be31b8..09207608908a45 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 b02f2ec9c76105..86a9a24fab6de6 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 ef3b3928e0d9cd..9fa3e4c1cfe78a 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 f3a02cfe176e92..78fdab7eb8c5d3 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 184c75d88f3b8c..6e6c357e6cccc6 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 1bea45da51af90..87176bed5de114 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 cf5589fecdf438..011ba67a055121 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 = { diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da50247..e504d5f0b9a9fa 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 b2f43b315aa9b6..a5f1ca6107600e 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/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b22bb6dc713422..af63485507d05c 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 83a9cf23c85f3d..dc18b7929318bd 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 946f11024360f6..53ced59b17c5d0 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 2a88c1b7131329..8d1360aeaddada 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 bb0014f4278a11..f0f11558abd65c 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 baee0623f0b5ae..99baa30e18c7a7 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 00000000000000..3f12386281059d --- /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 00000000000000..69a1433b6505c0 --- /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 7178eccfec4b67..73de3b14f88f6a 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 29c62d5c60775e..ea3e35f607be41 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 9c33bbcbc200a6..0ff70585af1448 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 db5f40d8e13cba..f0303553dfac0a 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 7ee6cb56d99f27..02ab5abade7fba 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 b8e8fd0679baa6..b689db12969222 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 b73f7391bf22a4..ee06bcab6528b2 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 b97b6f46600ae2..4ade7d17684198 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/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2e..93ffaa93cd80e9 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 3433d076ee800a..ac38f300bd02b5 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 69531013abae4d..fd02f83d72823c 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 00d51da501834c..4f35c1c1e5fc1b 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 15efbf38e7b93a..6f74198bb56ab7 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 d5d57da400a519..a3a2331cf8f76f 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 00000000000000..85cf9422630d68 --- /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 00000000000000..b380b85f7f3564 --- /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 00000000000000..0fb74a41b5df0b --- /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 00000000000000..322ceacbe002ea --- /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 00000000000000..3d169531f57575 --- /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 4792ceefde536e..0a9d477c266914 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 00000000000000..0280637acc0999 --- /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 00000000000000..0a9c7320d43593 --- /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 00000000000000..8adc535e802f0e --- /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 e0919671135ea5..69521c20cddfed 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 00000000000000..3d0b58aa6c8a34 --- /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 00000000000000..be1f9d3a10cf76 --- /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 e31aae7fcdda72..1dcb7263c48182 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 2bb8f7214f9045..c1800a09ba35c9 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 59ae89300705ec..6900630ffa9710 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 8d6a8227203d26..042ffac583e987 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 eeacec0834ea62..1948792d55a83e 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 667350b693a54c..e97418581a42f1 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/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js deleted file mode 100644 index b7c59fce6dec21..00000000000000 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ /dev/null @@ -1,12 +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 * as vegaLite from 'vega-lite/build-es5/vega-lite'; -import * as vega from 'vega/build-es5/vega'; - -export { vega, vegaLite }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea991..7cc70f31589c7c 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 fb36a0097c9700..76479cbcdf1ec9 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 54d4cf16f0cde0..902f79d03e680e 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 2ef687594ce065..d9b1b536a6d171 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 f200d27e1b9674..3dc245f196774b 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 963c2bd03f415c..da4c14c77bc98a 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 884e948e2aea33..a3efba804b4548 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 29c8d33cf39673..2085e250045f62 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 b59e1c65ab3f80..21c18e15c242ca 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 1cdc3af7335895..4c155d6b5ea884 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 5d5f3ed3d37337..5b1e49a73343bf 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 a55d5c4423f0e0..776f8898b3e3a3 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 f1e97416d76657..affd93dedb8ca9 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 b3abc460701594..9db1b7657f4447 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 c013056ba4566f..d03ee6eae790e8 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 b266a681f80315..9d329c92bede0c 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 5be971a085d3ce..75a2f4fb6895c1 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/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca3968..3d82e6c60a1b6e 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 ) { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts index a6ecd491f169f4..8481e2bf334aa6 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 64ef11167b3338..b889b59fdaf329 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/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 6239b930434aff..57b7ff0935f587 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 e9514d7d55457a..77f84dee25ded1 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 d9e3c278695910..a5f5262196346e 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 355e5df1f18958..de31b621a64803 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 5247bc74131d43..0dfece825d3a10 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 32a72f374cbe17..5206d51054745c 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 be31e0faf1e467..66c2a083c79e51 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 f912a2efcf0d9c..84ab6e36956d5d 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 4fcce29905beb1..b203a2c7b7071b 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 ce14e9cea7b13f..631046a0564a36 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 87de59a94fd249..6ab2352ebb05f6 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 69c85428d06247..4dfd06a61eecf5 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 2b61ed7586384d..bc092dd3889bb8 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 2e42fbfc6ac60a..7e0564ac44a43b 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 23a0d3fb2cd3cc..b424cab9ff45b2 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 c2286f8ea3dce6..c287e73e3ace9c 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 1e80487da551a3..99007376e1ea49 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/common/config.js b/test/common/config.js index 451324d46f62de..9d108f05fd1fca 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,8 +61,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/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js index c2d01eef267bc8..9a06dd7b749699 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/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index d13ba0114a598a..e2a056359b48e2 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 b2d27002f690c5..eeb0b224d5f0ca 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 a58c1298104709..90dd8cdc35c300 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 27745654f495f9..8697dc49de46a9 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 7ccce2c10c7b1e..3102becbe181f6 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 15828295b190f0..d31245b5492d1c 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 4f4e6d6655be51..caa008080b2a30 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/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243d..8d75cb09469bc8 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 6a167b17befd13..48c8d85b21dac9 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 6c600aa996a33a..0e52b536410e4e 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 918e9f16c5dae7..7947616ac65684 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 10a088426c8fdd..da4c785342733b 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/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts index 229dac20390a29..44a0e2eb0e1212 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/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d0..8fbacc71d30cb3 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 f0524e67d324cc..da3afd03513f05 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 e2840dbdf5ef7a..49fded8649c469 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 6a814f68a67f49..3093d5d9b8d299 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 7387ae1a203c20..345f6099009fc3 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/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0d..8b4fdc73dab44e 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 eeb295b264f608..4c12aa3d92b47b 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 55ed72ca01957e..6abfb864d1cd08 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 c9197a30b5214e..eb7481d12387d0 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 b7d7b7c0e20d10..0a116545e6e366 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 79e49050941be4..3437920ed7c98a 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 b572db7ebfd4ca..3935b5bb2814b7 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 8c04410f9953bf..7987278f400ff9 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 f2d5a3c52daea6..62d95c1043800d 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 59c39aecddb329..ff9e27cad1869a 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 9ac537829a670d..eb58039ff58f7d 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 76235c917b1398..3aaf50fbeb3e69 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 019b94f638ca4a..d529e981aaea10 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 402058a776605f..fc1cc63146358e 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 75b6089cddf9b0..8aa35def387b73 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 19679f02df0ad8..24d13cf24ccfb5 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 059edd5edf1deb..2d0e7e519e3bd4 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 8097c22cfbabc6..53b020e5b82411 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 2324c31b13d004..823a6b0afa9dc8 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 bfc757accaa82f..e846b133c26e0b 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 4ea8ef2c089e4a..569479f921cddc 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 04be4ce67c12d7..9cc874735cc0e4 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 786f5ba587d267..0e5e62b591290c 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 111f294b6ad55e..0890274fed950c 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 4462edeed9510c..0de03e54e1f790 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 e96c257516b98d..6a9fd1da826a6a 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 57c42d887bc83c..2e18d427272ce5 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/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573df..50e647e271ecc7 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/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a342..7378d45e1bb3aa 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 2d7c884edad83c..22b5035378a20d 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 bf0cfd2d476dd8..d032945245faf0 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 411da6da0223c6..cd91e8c325c066 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 0215b8f27b3932..086a9411f20b8d 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 dfe5c19bc417b5..ca131efeff68cc 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -488,17 +488,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 +518,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 c1a6067195c979..5aec696f5e1440 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 a09107b90a0156..c639a9b0332ac6 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 5b3c2ea5ce7088..3d391cc89a7e31 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 b45b7836eb46f9..8dfeac11dacf3b 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 c59e2decebd992..5a1e86c15c0024 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 a773173b1ddc10..a03c35bdc6e737 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 9a60abdc69423d..bcebedae2e07a1 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 83fd139a1e8e82..6156212a632032 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 ca8378c117b7df..cd6e0ef61e3f08 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 624b7bbcae5721..01759c2015cdf6 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 36506d05905958..c80fd77fc11ecc 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 c9aa221edf4d29..caff15efff68c2 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 11991a971829a0..c3ceb4b7502e26 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 b8be02af101b42..c984a84ceea014 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 1000a1b1459328..90d85e98ecd679 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 7338c440483ea6..466870bead71ce 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 9f4373ab553ecf..62d59aada3b7ba 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 c75b91b3fbd11c..42d3aff2b0d702 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 b8c1b7befb443c..cd46cff0f8a174 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 e2fa83cf32b637..72d551a1229801 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 7475ad49681427..5105e145309827 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 97d48702cf4c6e..85812fee3885c8 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 d42cb19a340bdd..f5d0015297daa0 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 b3beab546c811b..014bc58e747ea4 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 5cdadeb0c82d84..65051163c78c3a 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/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index f982332886e943..170bec54983c0e 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 9999ab91e31b22..77ce882275b6bf 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 ab24c26e0cdfae..f19ad4e7fe417d 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 0c35fc29e01cd7..fda1568c56e0e6 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 8d560d9a0f5534..d7c06a92f70e07 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 5c13756842039a..32dac5fba86f9a 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 532cf253c7f89b..95793c0cad4655 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 76533a476561b9..e6baca305508ea 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 5e439c31bbdc9e..2b5461d71500e2 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 2d3b6a7c45d075..764f41966261c6 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 3664be3b4903a9..068c33ea2c31f5 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 9c3c89294f403b..20e482d9df5b5e 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 a2dd383695983e..f1a793d11166ca 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 62093dbfe53ece..dfaf0a490225a1 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 22352b97da0ea3..d94e7faa0eabff 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/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 8932388398b6a0..acc6ae7af2727a 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 c76ff798b1286c..d6934c6846b79c 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 5c57ef11380e59..9f350610b1366d 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 e91e085207cb75..99555fa56acd59 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 00000000000000..a8a0e2c7119a91 --- /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/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 25275ba8e2249b..2a6228f16867dc 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 77fda43c37fef9..9625a814c79589 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/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f793102738..1db463a47dbf0e 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 bc064e236b658e..b79d6a0b89a575 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/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b0..7440b6ee1e1dfa 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/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a44..b950b064774b19 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 012c050cd3fa8a..97e572d15327c4 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 2a5138d0d88801..08224980a558fa 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 6fd9e7534ac650..654c3de7d81a96 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 0fd30189c54159..bb0b616d37eac3 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/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29c..a5843d1c4ade14 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 19859ab05e6a97..d52f299a692cf5 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 77509275431e9b..c30f4ac057ec0c 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 aefb5429a1b131..996adfdc13f647 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 ba78cf411e3691..2b8d9afe979e89 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 2d8e812408efe2..c2a9bfa6506e59 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 cebcbb1a0dc92c..ed85febac9a45b 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 6026e42676c57e..66b916ac7f70fe 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 84fc410c72cd01..d57951ecb5b1d4 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 3156af7e930bda..cccfa7de6d177d 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 3791c63f662ae7..4658e6774b7269 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 6b58ca71f7f4e7..a2aff41b68df70 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 95563c7c48ef58..3dbaa137bb9281 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 0d5d83582b42ba..8c35fd2ce8f8ba 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/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf602944..c93b5dbc4c36de 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 9d2a075dc35f9e..fb6c00c2f6f480 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 d576858c98e36b..d1bf4d51700ba8 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 d9d32dd68c95d7..4b26b1016d5301 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/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb802..1343c46ecdd729 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 77031d47649688..0a879ce92cba6e 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 62b8b75a38d737..149d111b08f02a 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/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08f..3933751105cb44 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/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index d7c0a465dd3e0e..8bbbecf8108feb 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -50,10 +50,7 @@ export class UptimePlugin implements Plugin { 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/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e759..ceade131fc5afc 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 9bd42171c75d5d..ffef7117bbbd8a 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/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ec1f37ca02be2f..d7a9cfc0d08b40 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/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index cef1bdbba754b8..3653d9916466d0 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/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 55474bd5a76884..5b2632ef710e4d 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/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 bb60de86aef82b..95ebc7b2ff5d57 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 f085bad4c507ec..b63ae2295f70b5 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 fe29bad0fa3814..26b033e28b4da7 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/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 5946a502a4ce37..59d6074d9d8caf 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 415cdf4814176c..ed4bc8f4f8c7b7 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 69b3e05946345a..bf79d35178a60d 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 10943b3a2929f2..0a7a30f373e07f 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 6fabd16752dfab..5d51c2923abd02 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 e35cfe4e024a26..ae88ab6486e64c 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 7cf481ca81a442..ec6cf338a43da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17000,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" @@ -19422,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" @@ -29569,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== @@ -29603,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== @@ -29747,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== @@ -29776,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"