diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8ad02b7162b6ab..669395564db447 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,10 +3,13 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -// Looks like 'oss:ciGroup:1' or 'oss:firefoxSmoke' -def JOB_PARTS = params.CI_GROUP.split(':') +def CI_GROUP_PARAM = params.CI_GROUP + +// Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke', or 'all:serverMocha' +def JOB_PARTS = CI_GROUP_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' def JOB = JOB_PARTS[1] +def NEED_BUILD = JOB != 'serverMocha' def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) @@ -31,13 +34,15 @@ stage("Kibana Pipeline") { print "Agent ${agentNumberInside} - ${agentExecutions} executions" kibanaPipeline.withWorkers('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() } }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() } @@ -61,7 +66,17 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { - if (job == 'firefoxSmoke') { + if (job == 'serverMocha') { + return kibanaPipeline.getPostBuildWorker('serverMocha', { + kibanaPipeline.bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/mocha + """, + "run `node scripts/mocha`" + ) + }) + } else if (job == 'firefoxSmoke') { return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e2abd5a3db1c3..e208dc73c7b4ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -61,6 +61,7 @@ /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform +/packages/kbn-config-schema/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 9124b4a1b21c4c..5202e0cfd5ebb7 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete:

(route: RouteConfig, handler: RequestHandler) => void; +delete: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 0291906c6fc6b9..32552a49cb999a 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get:

(route: RouteConfig, handler: RequestHandler) => void; +get: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md new file mode 100644 index 00000000000000..23674200680644 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) + +## IRouter.handleLegacyErrors property + +Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + +Signature: + +```typescript +handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index bbffe1e42f229a..b5d3c893d745dc 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,9 +16,10 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | -| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar | Register a route handler for GET request. | +| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index e97a32e433ce95..cd655c9ce0dc8b 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post:

(route: RouteConfig, handler: RequestHandler) => void; +post: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index 25db91e3899397..e553d4b79dd2b3 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put:

(route: RouteConfig, handler: RequestHandler) => void; +put: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index b7054dde95fc1c..13e0ea3645f26a 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -171,6 +171,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | +| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 36d803ddea6188..248726e26f3935 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,6 +16,5 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) | Map<PluginName, Observable<unknown>> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
} | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
internal: Map<PluginName, InternalPluginInfo>;
public: Map<PluginName, DiscoveredPlugin>;
browserConfigs: Map<PluginName, Observable<unknown>>;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md deleted file mode 100644 index 4bd57b873043e8..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) - -## PluginsServiceSetup.uiPluginConfigs property - -Signature: - -```typescript -uiPluginConfigs: Map>; -``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index fa286dfb59092e..7c47304cb9bf6f 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -8,7 +8,8 @@ ```typescript uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md new file mode 100644 index 00000000000000..535927dc73743f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) + +## RouteRegistrar type + +Handler to declare a route. + +Signature: + +```typescript +export declare type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index b852d38c05dc98..a2c05e4d873250 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -20,7 +20,7 @@ are enabled. Do not set this to `false`; it disables the login form, user and role management screens, and authorization using <>. To disable {security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. +{ref}/security-settings.html[{es} security settings]. `xpack.security.audit.enabled`:: Set to `true` to enable audit logging for security events. By default, it is set @@ -40,7 +40,7 @@ An arbitrary string of 32 characters or more that is used to encrypt credentials in a cookie. It is crucial that this key is not exposed to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. -In addition, high-availability deployments of {kib} will behave unexpectedly +In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: @@ -49,7 +49,16 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -`xpack.security.sessionTimeout`:: +`xpack.security.session.idleTimeout`:: Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit timeout, closing the -browser still requires the user to log back in to {kib}. +until the browser is closed. When this is set to an explicit idle timeout, closing +the browser still requires the user to log back in to {kib}. + +`xpack.security.session.lifespan`:: +Sets the maximum duration (in milliseconds), also known as "absolute timeout". By +default, a session can be renewed indefinitely. When this value is set, a session +will end once its lifespan is exceeded, even if the user is not idle. NOTE: if +`idleTimeout` is not set, this setting will still cause sessions to expire. + +`xpack.security.loginAssistanceMessage`:: +Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c2b1adc5e1b921..e6b70fa059fc28 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -188,9 +188,10 @@ The following sections apply both to <> and <> Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider -for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` -setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie -can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged +out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will +automatically renew it with a one-time-use refresh token and store it in the same cookie. {kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 1c74bd98642a7c..2fbc6ba4f1ee64 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters" For more information, see <>. -- -. Optional: Change the default session duration. By default, sessions stay -active until the browser is closed. To change the duration, set the -`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file. -The timeout is specified in milliseconds. For example, set the timeout to 600000 -to expire sessions after 10 minutes: +. Optional: Set a timeout to expire idle sessions. By default, a session stays +active until the browser is closed. To define a sliding session expiration, set +the `xpack.security.session.idleTimeout` property in the `kibana.yml` +configuration file. The idle timeout is specified in milliseconds. For example, +set the idle timeout to 600000 to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.sessionTimeout: 600000 +xpack.security.session.idleTimeout: 600000 +-------------------------------------------------------------------------------- +-- + +. Optional: Change the maximum session duration or "lifespan" -- also known as +the "absolute timeout". By default, a session stays active until the browser is +closed. If an idle timeout is defined, a session can still be extended +indefinitely. To define a maximum session lifespan, set the +`xpack.security.session.lifespan` property in the `kibana.yml` configuration +file. The lifespan is specified in milliseconds. For example, set the lifespan +to 28800000 to expire sessions after 8 hours: ++ +-- +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.session.lifespan: 28800000 -------------------------------------------------------------------------------- -- diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md new file mode 100644 index 00000000000000..8ba2c43b5e1fe6 --- /dev/null +++ b/packages/kbn-config-schema/README.md @@ -0,0 +1,511 @@ +# `@kbn/config-schema` — The Kibana config validation library + +`@kbn/config-schema` is a TypeScript library inspired by Joi and designed to allow run-time validation of the +Kibana configuration entries providing developers with a fully typed model of the validated data. + +## Table of Contents + +- [Why `@kbn/config-schema`?](#why-kbnconfig-schema) +- [Schema building blocks](#schema-building-blocks) + - [Basic types](#basic-types) + - [`schema.string()`](#schemastring) + - [`schema.number()`](#schemanumber) + - [`schema.boolean()`](#schemaboolean) + - [`schema.literal()`](#schemaliteral) + - [Composite types](#composite-types) + - [`schema.arrayOf()`](#schemaarrayof) + - [`schema.object()`](#schemaobject) + - [`schema.recordOf()`](#schemarecordof) + - [`schema.mapOf()`](#schemamapof) + - [Advanced types](#advanced-types) + - [`schema.oneOf()`](#schemaoneof) + - [`schema.any()`](#schemaany) + - [`schema.maybe()`](#schemamaybe) + - [`schema.nullable()`](#schemanullable) + - [`schema.never()`](#schemanever) + - [`schema.uri()`](#schemauri) + - [`schema.byteSize()`](#schemabytesize) + - [`schema.duration()`](#schemaduration) + - [`schema.conditional()`](#schemaconditional) + - [References](#references) + - [`schema.contextRef()`](#schemacontextref) + - [`schema.siblingRef()`](#schemasiblingref) +- [Custom validation](#custom-validation) +- [Default values](#default-values) + +## Why `@kbn/config-schema`? + +Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates. + +There are a number of reasons why we decided to roll our own solution for the configuration validation: + +* **Limited API surface** - having a future rich library is awesome, but it's a really hard task to audit such library and make sure everything is sane and secure enough. As everyone knows complexity is the enemy of security and hence we'd like to have a full control over what exactly we expose and commit to maintain. +* **Custom error messages** - detailed validation error messages are a great help to developers, but at the same time they can contain information that's way too sensitive to expose to everyone. We'd like to control these messages and make them only as detailed as really needed. For example, we don't want validation error messages to contain the passwords for internal users to show-up in the logs. These logs are commonly ingested into Elasticsearch, and accessible to a large number of users which shouldn't have access to the internal user's password. +* **Type information** - having run-time guarantees is great, but additionally having compile-time guarantees is even better. We'd like to provide developers with a fully typed model of the validated data so that it's harder to misuse it _after_ validation. +* **Upgradability** - no matter how well a validation library is implemented, it will have bugs and may need to be improved at some point anyway. Some external libraries are very well supported, some aren't or won't be in the future. It's always a risk to depend on an external party with their own release cadence when you need to quickly fix a security vulnerability in a patch version. We'd like to have a better control over lifecycle of such an important piece of our codebase. + +## Schema building blocks + +The schema is composed of one or more primitives depending on the shape of the data you'd like to validate. + +```typescript +const simpleStringSchema = schema.string(); +const moreComplexObjectSchema = schema.object({ name: schema.string() }); +``` + +Every schema instance has a `validate` method that is used to perform a validation of the data according to the schema. This method accepts three arguments: + +* `data: any` - **required**, data to be validated with the schema +* `context: Record` - **optional**, object whose properties can be referenced by the [context references](#schemacontextref) +* `namespace: string` - **optional**, arbitrary string that is used to prefix every error message thrown during validation + +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean(), + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); + +expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({ + isEnabled: true, + env: 'prod', +}); + +// Use default value for `env` from context via reference +expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({ + isEnabled: true, + env: 'staging', +}); + +// Fail because of type mismatch +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }) +).toThrowError( + '[isEnabled]: expected value of type [boolean] but got [string]' +); + +// Fail because of type mismatch and prefix error with a custom namespace +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration') +).toThrowError( + '[configuration.isEnabled]: expected value of type [boolean] but got [string]' +); +``` + +__Notes:__ +* `validate` method throws as soon as the first schema violation is encountered, no further validation is performed. +* when you retrieve configuration within a Kibana plugin `validate` function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.). + +### Basic types + +#### `schema.string()` + +Validates input data as a string. + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minLength: number` - defines a minimum length the string should have. + * `maxLength: number` - defines a maximum length the string should have. + * `hostname: boolean` - indicates whether the string should be validated as a valid hostname (per [RFC 1123](https://tools.ietf.org/html/rfc1123)). + +__Usage:__ +```typescript +const valueSchema = schema.string({ maxLength: 10 }); +``` + +__Notes:__ +* By default `schema.string()` allows empty strings, to prevent that use non-zero value for `minLength` option. +* To validate a string using a regular expression use a custom validator function, see [Custom validation](#custom-validation) section for more details. + +#### `schema.number()` + +Validates input data as a number. + +__Output type:__ `number` + +__Options:__ + * `defaultValue: number | Reference | (() => number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: number` - defines a minimum value the number should have. + * `max: number` - defines a maximum value the number should have. + +__Usage:__ +```typescript +const valueSchema = schema.number({ max: 10 }); +``` + +__Notes:__ +* The `schema.number()` also supports a string as input if it can be safely coerced into number. + +#### `schema.boolean()` + +Validates input data as a boolean. + +__Output type:__ `boolean` + +__Options:__ + * `defaultValue: boolean | Reference | (() => boolean)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: boolean) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.boolean({ defaultValue: false }); +``` + +#### `schema.literal()` + +Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. + +__Output type:__ `string`, `number` or `boolean` literals + +__Options:__ + * `defaultValue: TLiteral | Reference | (() => TLiteral)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TLiteral) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = [ + schema.literal('stringLiteral'), + schema.literal(100500), + schema.literal(false), +]; +``` + +### Composite types + +#### `schema.arrayOf()` + +Validates input data as a homogeneous array with the values being validated against predefined schema. + +__Output type:__ `TValue[]` + +__Options:__ + * `defaultValue: TValue[] | Reference | (() => TValue[])` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minSize: number` - defines a minimum size the array should have. + * `maxSize: number` - defines a maximum size the array should have. + +__Usage:__ +```typescript +const valueSchema = schema.arrayOf(schema.number()); +``` + +#### `schema.object()` + +Validates input data as an object with a predefined set of properties. + +__Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` + +__Options:__ + * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean({ defaultValue: false }), + name: schema.string({ minLength: 10 }), +}); +``` + +__Notes:__ +* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. + +#### `schema.recordOf()` + +Validates input data as an object with the keys and values being validated against predefined schema. + +__Output type:__ `Record` + +__Options:__ + * `defaultValue: Record | Reference> | (() => Record)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Record) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.recordOf(schema.string(), schema.number()); +``` + +__Notes:__ +* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. + +#### `schema.mapOf()` + +Validates input data as a map with the keys and values being validated against the predefined schema. + +__Output type:__ `Map` + +__Options:__ + * `defaultValue: Map | Reference> | (() => Map)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Map) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.mapOf(schema.string(), schema.number()); +``` + +### Advanced types + +#### `schema.oneOf()` + +Allows a list of alternative schemas to validate input data against. + +__Output type:__ `TValue1 | TValue2 | TValue3 | ..... as TUnion` + +__Options:__ + * `defaultValue: TUnion | Reference | (() => TUnion)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]); +``` + +__Notes:__ +* Since the result data type is a type union you should use various TypeScript type guards to get the exact type. + +#### `schema.any()` + +Indicates that input data shouldn't be validated and returned as is. + +__Output type:__ `any` + +__Options:__ + * `defaultValue: any | Reference | (() => any)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: any) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.any(); +``` + +__Notes:__ +* `schema.any()` is essentially an escape hatch for the case when your data can __really__ have any type and should be avoided at all costs. + +#### `schema.maybe()` + +Indicates that input data is optional and may not be present. + +__Output type:__ `T | undefined` + +__Usage:__ +```typescript +const valueSchema = schema.maybe(schema.string()); +``` + +__Notes:__ +* Don't use `schema.maybe()` if a nested type defines a default value. + +#### `schema.nullable()` + +Indicates that input data is optional and defaults to `null` if it's not present. + +__Output type:__ `T | null` + +__Usage:__ +```typescript +const valueSchema = schema.nullable(schema.string()); +``` + +__Notes:__ +* `schema.nullable()` also treats explicitly specified `null` as a valid input. + +#### `schema.never()` + +Indicates that input data is forbidden. + +__Output type:__ `never` + +__Usage:__ +```typescript +const valueSchema = schema.never(); +``` + +__Notes:__ +* `schema.never()` has a very limited application and usually used within [conditional schemas](#schemaconditional) to fully or partially forbid input data. + +#### `schema.uri()` + +Validates input data as a proper URI string (per [RFC 3986](https://tools.ietf.org/html/rfc3986)). + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `scheme: string | string[]` - limits allowed URI schemes to the one(s) defined here. + +__Usage:__ +```typescript +const valueSchema = schema.uri({ scheme: 'https' }); +``` + +__Notes:__ +* Prefer using `schema.uri()` for all URI validations even though it may be possible to replicate it with a custom validator for `schema.string()`. + +#### `schema.byteSize()` + +Validates input data as a proper digital data size. + +__Output type:__ `ByteSizeValue` + +__Options:__ + * `defaultValue: ByteSizeValue | string | number | Reference | (() => ByteSizeValue | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: ByteSizeValue | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: ByteSizeValue | string | number` - defines a minimum value the size should have. + * `max: ByteSizeValue | string | number` - defines a maximum value the size should have. + +__Usage:__ +```typescript +const valueSchema = schema.byteSize({ min: '3kb' }); +``` + +__Notes:__ +* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`. +* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. +* Currently you cannot specify zero bytes with a string format and should use number `0` instead. + +#### `schema.duration()` + +Validates input data as a proper [duration](https://momentjs.com/docs/#/durations/). + +__Output type:__ `moment.Duration` + +__Options:__ + * `defaultValue: moment.Duration | string | number | Reference | (() => moment.Duration | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: moment.Duration | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.duration({ defaultValue: '70ms' }); +``` + +__Notes:__ +* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. +* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. + +#### `schema.conditional()` + +Allows a specified condition that is evaluated _at the validation time_ and results in either one or another input validation schema. + +The first argument is always a [reference](#references) while the second one can be: +* another reference, in this cases both references are "dereferenced" and compared +* schema, in this case the schema is used to validate "dereferenced" value of the first reference +* value, in this case "dereferenced" value of the first reference is compared to that value + +The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to `true`, otherwise `schema.conditional()` should fallback +to the schema provided as the fourth argument. + +__Output type:__ `TTrueResult | TFalseResult` + +__Options:__ + * `defaultValue: TTrueResult | TFalseResult | Reference | (() => TTrueResult | TFalseResult` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TTrueResult | TFalseResult) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + key: schema.oneOf([schema.literal('number'), schema.literal('string')]), + value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), +}); +``` + +__Notes:__ +* Conditional schemas may be hard to read and understand and hence should be used only sparingly. + +### References + +#### `schema.contextRef()` + +Defines a reference to the value specified through the validation context. Context reference is only used as part of a [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); +valueSchema.validate({}, { envName: 'dev' }); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. +* The root context that Kibana provides during config validation includes lots of useful properties like `environment name` that can be used to provide a strict schema for production and more relaxed one for development. + +#### `schema.siblingRef()` + +Defines a reference to the value of the sibling key. Sibling references are only used a part of [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + node: schema.object({ tag: schema.string() }), + env: schema.string({ defaultValue: schema.siblingRef('node.tag') }), +}); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. + +## Custom validation + +Using built-in schema primitives may not be enough in some scenarios or sometimes the attempt to model complex schemas with built-in primitives only may result in unreadable code. +For these cases `@kbn/config-schema` provides a way to specify a custom validation function for almost any schema building block through the `validate` option. + +For example `@kbn/config-schema` doesn't have a dedicated primitive for the `RegExp` based validation currently, but you can easily do that with a custom `validate` function: + +```typescript +const valueSchema = schema.string({ + minLength: 3, + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, +}); + +// ...or if you use that construct a lot... + +const regexSchema = (regex: RegExp) => schema.string({ + validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`, +}); +const valueSchema = regexSchema(/^[a-z0-9_-]+$/); +``` + +Custom validation function is run _only after_ all built-in validations passed. It should either return a `string` as an error message +to denote the failed validation or not return anything at all (`void`) otherwise. Please also note that `validate` function is synchronous. + +Another use case for custom validation functions is when the schema depends on some run-time data: + +```typescript +const gesSchema = randomRunTimeSeed => schema.string({ + validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined +}); + +const schema = gesSchema('some-random-run-time-data'); +``` + +## Default values + +If you have an optional config field that you can have a default value for you may want to consider using dedicated `defaultValue` option to not +deal with "defined or undefined"-like checks all over the place in your code. You have three options to provide a default value for almost any schema primitive: + +* plain value that's known at the compile time +* [reference](#references) to a value that will be "dereferenced" at the validation time +* function that is invoked at the validation time and returns a plain value + +```typescript +const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' }); +const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') }); +const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() }); +``` + +__Notes:__ +* `@kbn/config-schema` neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type. diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index b43dcd80b44624..aa6611f3b6738d 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -70,11 +70,21 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug describe(`then running`, () => { it(`'yarn test:browser' should exit 0`, async () => { - await execa('yarn', ['test:browser'], { cwd: generatedPath }); + await execa('yarn', ['test:browser'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { cwd: generatedPath }); + await execa('yarn', ['test:server'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn build' should exit 0`, async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 52672d5f039fbe..4530b614236207 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -143,7 +143,7 @@ export const schema = Joi.object() junit: Joi.object() .keys({ - enabled: Joi.boolean().default(!!process.env.CI), + enabled: Joi.boolean().default(!!process.env.CI && !process.env.DISABLE_JUNIT_REPORTER), reportName: Joi.string(), }) .default(), diff --git a/packages/kbn-test/src/mocha/auto_junit_reporter.js b/packages/kbn-test/src/mocha/auto_junit_reporter.js index 50b589fbc57a59..b6e79616e1cde0 100644 --- a/packages/kbn-test/src/mocha/auto_junit_reporter.js +++ b/packages/kbn-test/src/mocha/auto_junit_reporter.js @@ -29,7 +29,7 @@ export function createAutoJUnitReporter(junitReportOptions) { new MochaSpecReporter(runner, options); // in CI we also setup the JUnit reporter - if (process.env.CI) { + if (process.env.CI && !process.env.DISABLE_JUNIT_REPORTER) { setupJUnitReportGeneration(runner, junitReportOptions); } } diff --git a/packages/kbn-test/src/mocha/run_mocha_cli.js b/packages/kbn-test/src/mocha/run_mocha_cli.js index 7a901084727217..77f40aded1d7fa 100644 --- a/packages/kbn-test/src/mocha/run_mocha_cli.js +++ b/packages/kbn-test/src/mocha/run_mocha_cli.js @@ -63,7 +63,16 @@ export function runMochaCli() { if (!opts._.length) { globby .sync( - ['src/**/__tests__/**/*.js', 'packages/**/__tests__/**/*.js', 'tasks/**/__tests__/**/*.js'], + [ + 'src/**/__tests__/**/*.js', + 'packages/**/__tests__/**/*.js', + 'tasks/**/__tests__/**/*.js', + 'x-pack/common/**/__tests__/**/*.js', + 'x-pack/server/**/__tests__/**/*.js', + `x-pack/legacy/plugins/*/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/common/**/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/**/server/**/__tests__/**/*.js`, + ], { cwd: REPO_ROOT, onlyFiles: true, diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 9f4e678c6adf5a..b65cd3835cc0ae 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), - require.resolve('../test/interpreter_functional/config.js'), + require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), ]); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 22c96110742e0e..c5e04c3cfb53ac 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,7 +1167,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/apply_filters'` | `import { applyFiltersPopover } from '../data/public'` | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9eed3a59acaa62..ccf14879baa379 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -298,6 +298,35 @@ class Plugin { } } ``` +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` +as a temporary solution until error migration is complete: +```ts +// legacy/plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; + +export interface DemoPluginsSetup {}; + +class Plugin { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + router.wrapErrors((context, req, res) => { + throw Boom.notFound('not there'); // will be converted into proper New Platform error + }) + ) + } +} +``` + #### 4. New Platform plugin As the final step we delete the shim and move all our code into a New Platform diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 00c9aedc42cfb7..e9a2571382edc4 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({ put: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), + handleLegacyErrors: jest.fn().mockImplementation(handler => handler), }); const createSetupContractMock = () => { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 895c899b008bb1..bed76201bb4f99 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 70d7ae00f917e6..481d8e1bbf49bf 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -164,6 +164,53 @@ describe('Handler', () => { }); }); +describe('handleLegacyErrors', () => { + it('properly convert Boom errors', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false }, + router.handleLegacyErrors((context, req, res) => { + throw Boom.notFound(); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('returns default error when non-Boom errors are thrown', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { + path: '/', + validate: false, + }, + router.handleLegacyErrors((context, req, res) => { + throw new Error('Unexpected'); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + }); +}); + describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts new file mode 100644 index 00000000000000..aa20b49dc9c91b --- /dev/null +++ b/src/core/server/http/router/error_wrapper.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; +import { wrapErrors } from './error_wrapper'; +import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server'; + +const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); +}; + +describe('wrapErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + 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 = wrapErrors(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 = wrapErrors(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 = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBeInstanceOf(KibanaResponse); + 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 = wrapErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); +}); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts new file mode 100644 index 00000000000000..706a9fe3b88871 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from './request'; +import { KibanaResponseFactory } from './response'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '../../../server'; + +export const wrapErrors =

( + handler: RequestHandler +): RequestHandler => { + return async ( + context: RequestHandlerContext, + request: KibanaRequest, TypeOf, TypeOf>, + response: KibanaResponseFactory + ) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e)) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 56ed9ca11edc13..f07ad3cfe85c03 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter } from './router'; +export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 6b7e2e3ad14cd3..a13eae51a19a61 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -27,6 +27,7 @@ import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from '. import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; +import { wrapErrors } from './error_wrapper'; interface RouterRoute { method: RouteMethod; @@ -35,6 +36,15 @@ interface RouterRoute { handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } +/** + * Handler to declare a route. + * @public + */ +export type RouteRegistrar =

( + route: RouteConfig, + handler: RequestHandler +) => void; + /** * Registers route handlers for specified resource path and method. * See {@link RouteConfig} and {@link RequestHandler} for more information about arguments to route registrations. @@ -52,40 +62,36 @@ export interface IRouter { * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - get:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + get: RouteRegistrar; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - post:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + post: RouteRegistrar; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - put:

( - route: RouteConfig, - handler: RequestHandler - ) => void; + put: RouteRegistrar; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - delete:

( - route: RouteConfig, + delete: RouteRegistrar; + + /** + * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + * @param handler {@link RequestHandler} - a route handler to wrap + */ + handleLegacyErrors:

( handler: RequestHandler - ) => void; + ) => RequestHandler; /** * Returns all routes registered with the this router. @@ -188,6 +194,12 @@ export class Router implements IRouter { return [...this.routes]; } + public handleLegacyErrors

( + handler: RequestHandler + ): RequestHandler { + return wrapErrors(handler); + } + private async handle

({ routeSchemas, request, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 265612d3fc4a9a..b53f04d601ff42 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -114,6 +114,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, SessionStorage, SessionStorageCookieOptions, SessionCookieValidationResult, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 1240518422e2fb..030caa8324521c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -41,7 +41,7 @@ import { configServiceMock } from '../config/config_service.mock'; import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; +import { DiscoveredPlugin } from '../plugins'; import { KibanaMigrator } from '../saved_objects/migrations'; import { ISavedObjectsClientProvider } from '../saved_objects'; @@ -84,9 +84,9 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + browserConfigs: new Map(), }, - uiPluginConfigs: new Map(), }, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index e86e6cde6e927d..99963ad9ce3e89 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -278,7 +278,6 @@ export class LegacyService implements CoreService { hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, - uiPluginConfigs: setupDeps.core.plugins.uiPluginConfigs, elasticsearch: setupDeps.core.elasticsearch, uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index e3be8fbb983090..8d3c6a8c909a2f 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -30,10 +30,10 @@ const createServiceMock = () => { mocked.setup.mockResolvedValue({ contracts: new Map(), uiPlugins: { - public: new Map(), + browserConfigs: new Map(), internal: new Map(), + public: new Map(), }, - uiPluginConfigs: new Map(), }); mocked.start.mockResolvedValue({ contracts: new Map() }); return mocked; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index da6d1d5a010e7a..7e55faa43360e4 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,11 +33,12 @@ import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; import { take } from 'rxjs/operators'; -import { DiscoveredPluginInternal } from './types'; +import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; +let config$: BehaviorSubject; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -107,11 +108,10 @@ describe('PluginsService', () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - configService = new ConfigService( - new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), - env, - logger + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ plugins: { initialize: true } }) ); + configService = new ConfigService(config$, env, logger); await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); @@ -198,7 +198,7 @@ describe('PluginsService', () => { .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), @@ -390,11 +390,10 @@ describe('PluginsService', () => { }); describe('#generateUiPluginsConfigs()', () => { - const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPluginInternal] => [ + const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPlugin] => [ plugin.name, { id: plugin.name, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: [], optionalPlugins: [], @@ -427,15 +426,14 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - const uiConfig$ = uiPluginConfigs.get('plugin-with-expose'); + const uiConfig$ = browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); @@ -468,15 +466,55 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - expect([...uiPluginConfigs.entries()]).toHaveLength(0); + expect([...browserConfigs.entries()]).toHaveLength(0); + }); + }); + + describe('#setup()', () => { + describe('uiPlugins.internal', () => { + it('includes disabled plugins', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-1', { + path: 'path-1', + version: 'some-version', + configPath: 'plugin1', + }), + createPlugin('plugin-2', { + path: 'path-2', + version: 'some-version', + configPath: 'plugin2', + }), + ]), + }); + + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + config$.next( + new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) + ); + + await pluginsService.discover(); + const { uiPlugins } = await pluginsService.setup({} as any); + expect(uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1" => Object { + "entryPointPath": "path-1/public", + }, + "plugin-2" => Object { + "entryPointPath": "path-2/public", + }, + } + `); + }); }); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 79c9489a8b4c00..4c73c2a304dc42 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,12 +25,7 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { - DiscoveredPlugin, - DiscoveredPluginInternal, - PluginConfigDescriptor, - PluginName, -} from './types'; +import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup } from '../internal_types'; @@ -41,10 +36,22 @@ import { pick } from '../../utils'; export interface PluginsServiceSetup { contracts: Map; uiPlugins: { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ public: Map; - internal: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; }; - uiPluginConfigs: Map>; } /** @public */ @@ -65,6 +72,7 @@ export class PluginsService implements CoreService; private readonly pluginConfigDescriptors = new Map(); + private readonly uiPluginInternalInfo = new Map(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); @@ -103,8 +111,11 @@ export class PluginsService implements CoreService { expect(thirdPluginToRun.setup).toHaveBeenCalledTimes(1); }); -test('`uiPlugins` returns empty Maps before plugins are added', async () => { - expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(` - Object { - "internal": Map {}, - "public": Map {}, - } - `); +test('`uiPlugins` returns empty Map before plugins are added', async () => { + expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(`Map {}`); }); test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { @@ -351,7 +346,7 @@ test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { pluginsSystem.addPlugin(plugin); }); - expect([...pluginsSystem.uiPlugins().internal.keys()]).toMatchInlineSnapshot(` + expect([...pluginsSystem.uiPlugins().keys()]).toMatchInlineSnapshot(` Array [ "order-0", "order-1", @@ -380,7 +375,7 @@ test('`uiPlugins` returns only ui plugin dependencies', async () => { pluginsSystem.addPlugin(plugin); }); - const plugin = pluginsSystem.uiPlugins().internal.get('ui-plugin')!; + const plugin = pluginsSystem.uiPlugins().get('ui-plugin')!; expect(plugin.requiredPlugins).toEqual(['req-ui']); expect(plugin.optionalPlugins).toEqual(['opt-ui']); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 34acb66d4e9310..f437b51e5b07a7 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -17,12 +17,10 @@ * under the License. */ -import { pick } from 'lodash'; - import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -158,33 +156,22 @@ export class PluginsSystem { const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter( pluginName => this.plugins.get(pluginName)!.includesUiPlugin ); - const internal = new Map( + const publicPlugins = new Map( uiPluginNames.map(pluginName => { const plugin = this.plugins.get(pluginName)!; return [ pluginName, { id: pluginName, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: plugin.manifest.requiredPlugins.filter(p => uiPluginNames.includes(p)), optionalPlugins: plugin.manifest.optionalPlugins.filter(p => uiPluginNames.includes(p)), }, - ] as [PluginName, DiscoveredPluginInternal]; + ]; }) ); - const publicPlugins = new Map( - [...internal.entries()].map( - ([pluginName, plugin]) => - [ - pluginName, - pick(plugin, ['id', 'configPath', 'requiredPlugins', 'optionalPlugins']), - ] as [PluginName, DiscoveredPlugin] - ) - ); - - return { public: publicPlugins, internal }; + return publicPlugins; } /** diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 17704ce687b92d..fd487d9fe00aa1 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -169,15 +169,14 @@ export interface DiscoveredPlugin { } /** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. * @internal */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { +export interface InternalPluginInfo { /** - * Path on the filesystem where plugin was loaded from. + * Path to the client-side entrypoint file to be used to build the client-side + * bundle for a plugin. */ - readonly path: string; + readonly entryPointPath: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7cc9e24b56dbea..3bbcb85fea9e54 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -714,14 +714,15 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete:

(route: RouteConfig, handler: RequestHandler) => void; - get:

(route: RouteConfig, handler: RequestHandler) => void; + delete: RouteRegistrar; + get: RouteRegistrar; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; - post:

(route: RouteConfig, handler: RequestHandler) => void; - put:

(route: RouteConfig, handler: RequestHandler) => void; + handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; + post: RouteRegistrar; + put: RouteRegistrar; routerPath: string; } @@ -1014,11 +1015,10 @@ export interface PluginsServiceSetup { // (undocumented) contracts: Map; // (undocumented) - uiPluginConfigs: Map>; - // (undocumented) uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; } @@ -1100,6 +1100,9 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +// @public +export type RouteRegistrar =

(route: RouteConfig, handler: RequestHandler) => void; + // @public (undocumented) export interface SavedObject { attributes: T; @@ -1634,6 +1637,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:45:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts ``` diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 5c0462ce86fa94..b0e38b64814578 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -66,10 +66,38 @@ export const CleanClientModulesOnDLLTask = { // side code entries that were provided const serverDependencies = await getDependencies(baseDir, serverEntries); + // This fulfill a particular exceptional case where + // we need to keep loading a file from a node_module + // only used in the front-end like we do when using the file-loader + // in https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js + // + // manual list of exception modules + const manualExceptionModules = [ + 'mapbox-gl' + ]; + + // consider the top modules as exceptions as the entry points + // to look for other exceptions dependent on that one + const manualExceptionEntries = [ + ...manualExceptionModules.map(module => `${baseDir}/node_modules/${module}`) + ]; + + // dependencies for declared exception modules + const manualExceptionModulesDependencies = await getDependencies(baseDir, [ + ...manualExceptionEntries + ]); + + // final list of manual exceptions to add + const manualExceptions = [ + ...manualExceptionModules, + ...manualExceptionModulesDependencies + ]; + // Consider this as our whiteList for the modules we can't delete const whiteListedModules = [ ...serverDependencies, - ...kbnWebpackLoaders + ...kbnWebpackLoaders, + ...manualExceptions ]; // Resolve the client vendors dll manifest path diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 6609b905b81eca..0c8faf47411d4e 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,9 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.session.idleTimeout + xpack.security.session.lifespan + xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom ) diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 30501965bf1e77..7f51326ee46bb8 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -45,7 +45,7 @@ export default class JestJUnitReporter { * @return {undefined} */ onRunComplete(contexts, results) { - if (!process.env.CI || !results.testResults.length) { + if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } diff --git a/src/fixtures/stubbed_search_source.js b/src/fixtures/stubbed_search_source.js index 3a36b97e6757eb..da741a1aa47743 100644 --- a/src/fixtures/stubbed_search_source.js +++ b/src/fixtures/stubbed_search_source.js @@ -60,9 +60,6 @@ export default function stubSearchSource(Private, $q, Promise) { onRequestStart(fn) { this._requestStartHandlers.push(fn); }, - requestIsStarting(req) { - return Promise.map(this._requestStartHandlers, fn => fn(req)); - }, requestIsStopped() {} }; diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 1e13b6b4839818..b19fd438f8ee3e 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,5 +1,8 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). +@import '@elastic/eui/src/components/header/variables'; + #consoleRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); display: flex; flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container diff --git a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts index 39ec1f78b65f0b..946b3997a97129 100644 --- a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts +++ b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts @@ -29,10 +29,10 @@ import { esFilters, FilterManager, TimefilterContract, + applyFiltersPopover, changeTimeFilter, extractTimeFilter, } from '../../../../../../plugins/data/public'; -import { applyFiltersPopover } from '../apply_filters/apply_filters_popover'; import { IndexPatternsStart } from '../../index_patterns'; export const GLOBAL_APPLY_FILTER_ACTION = 'GLOBAL_APPLY_FILTER_ACTION'; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx deleted file mode 100644 index 41f757e726c40b..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React, { Component } from 'react'; -import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; -import { IndexPattern } from '../../index_patterns/index_patterns'; -import { esFilters } from '../../../../../../plugins/data/public'; - -interface Props { - filters: esFilters.Filter[]; - onCancel: () => void; - onSubmit: (filters: esFilters.Filter[]) => void; - indexPatterns: IndexPattern[]; -} - -interface State { - isFilterSelected: boolean[]; -} - -export class ApplyFiltersPopover extends Component { - public render() { - if (!this.props.filters || this.props.filters.length === 0) { - return ''; - } - - return ( - - - - - - ); - } -} - -type cancelFunction = () => void; -type submitFunction = (filters: esFilters.Filter[]) => void; -export const applyFiltersPopover = ( - filters: esFilters.Filter[], - indexPatterns: IndexPattern[], - onCancel: cancelFunction, - onSubmit: submitFunction -) => { - return ( - - ); -}; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index b33aef75e6756e..13491877790619 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, @@ -48,7 +47,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts index 5dcf4005ef4e86..db1ece78e7b4d1 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts @@ -33,7 +33,6 @@ const createSetupContractMock = () => { flattenHitWrapper: jest.fn().mockImplementation(flattenHitWrapper), formatHitProvider: jest.fn(), indexPatterns: jest.fn() as any, - IndexPatternSelect: jest.fn(), __LEGACY: { // For BWC we must temporarily export the class implementation of Field, // which is only used externally by the Index Pattern UI. diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index f97246bc5a9bf0..381cd491f02103 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -25,7 +25,6 @@ import { } from 'src/core/public'; import { FieldFormatsStart } from '../../../../../plugins/data/public'; import { Field, FieldList, FieldListInterface, FieldType } from './fields'; -import { createIndexPatternSelect } from './components'; import { setNotifications, setFieldFormats } from './services'; import { @@ -79,7 +78,6 @@ export class IndexPatternsService { return { ...this.setupApi, indexPatterns: new IndexPatterns(uiSettings, savedObjectsClient, http), - IndexPatternSelect: createIndexPatternSelect(savedObjectsClient), }; } @@ -91,7 +89,6 @@ export class IndexPatternsService { // static code /** @public */ -export { IndexPatternSelect } from './components'; export { CONTAINS_SPACES, getFromSavedObject, @@ -120,4 +117,4 @@ export type IndexPatternsStart = ReturnType; export { IndexPattern, IndexPatterns, StaticIndexPattern, Field, FieldType, FieldListInterface }; /** @public */ -export { getIndexPatternTitle, findIndexPatternByTitle } from './utils'; +export { findIndexPatternByTitle } from './utils'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 8542c1dcce24d2..8c2878a3ff9bad 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -71,19 +71,6 @@ export async function findIndexPatternByTitle( ); } -export async function getIndexPatternTitle( - client: SavedObjectsClientContract, - indexPatternId: string -): Promise> { - const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; - - if (savedObject.error) { - throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); - } - - return savedObject.attributes.title; -} - function indexPatternContainsSpaces(indexPattern: string): boolean { return indexPattern.includes(' '); } diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index db2a803ea1c61f..7165de026920d0 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -29,7 +29,12 @@ import { ExpressionFunction, KibanaDatatableColumn, } from 'src/plugins/expressions/public'; -import { SearchSource } from '../../../../../ui/public/courier/search_source'; +import { + SearchSource, + SearchSourceContract, + getRequestInspectorStats, + getResponseInspectorStats, +} from '../../../../../ui/public/courier'; // @ts-ignore import { FilterBarQueryFilterProvider, @@ -37,10 +42,6 @@ import { } from '../../../../../ui/public/filter_manager/query_filter'; import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier/utils/courier_inspector_utils'; import { calculateObjectHash } from '../../../../../ui/public/vis/lib/calculate_object_hash'; import { getTime } from '../../../../../ui/public/timefilter'; // @ts-ignore @@ -50,7 +51,7 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; export interface RequestHandlerParams { - searchSource: SearchSource; + searchSource: SearchSourceContract; aggs: AggConfigs; timeRange?: TimeRange; query?: Query; @@ -119,7 +120,7 @@ const handleCourierRequest = async ({ return aggs.toDsl(metricsAtAllLevels); }); - requestSearchSource.onRequestStart((paramSearchSource: SearchSource, options: any) => { + requestSearchSource.onRequestStart((paramSearchSource, options) => { return aggs.onSearchRequestStart(paramSearchSource, options); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js index 27f37421b0e25b..45981adf9af453 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js @@ -17,8 +17,27 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('../../../../../core_plugins/data/public/legacy', () => ({ + indexPatterns: { + indexPatterns: { + get: jest.fn(), + } + } +})); + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return

; + } + } + } + }, + }, +})); import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js index 663a36ab69f466..c48123f3db714b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js @@ -20,12 +20,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { IndexPatternSelect } from 'ui/index_patterns'; - import { EuiFormRow, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function IndexPatternSelectFormRowUi(props) { const { controlIndex, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index ea029af9e48908..b37e8af0895fe0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -17,12 +17,24 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; + import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js index 5a698d65286ac2..8d601f5a727d13 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js @@ -17,19 +17,30 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
; + } + } + } + }, + }, +})); + import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; -import { - RangeControlEditor, -} from './range_control_editor'; +import { RangeControlEditor } from './range_control_editor'; const controlParams = { id: '1', diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js index 61a3d4084ab8f1..2ab4131957c320 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js @@ -19,9 +19,9 @@ import { timefilter } from 'ui/timefilter'; export function createSearchSource(SearchSource, initialState, indexPattern, aggs, useTimeFilter, filters = []) { - const searchSource = new SearchSource(initialState); + const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals - searchSource.setParent(false); + searchSource.setParent(undefined); searchSource.setField('filter', () => { const activeFilters = [...filters]; if (useTimeFilter) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts index 57391223fa1472..9c50adeeefccbd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -48,7 +48,7 @@ import { // @ts-ignore import { initDashboardApp } from './legacy_app'; import { DataStart } from '../../../data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; @@ -68,7 +68,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataStart['search']['services']['savedQueryService']; - embeddables: ReturnType; + embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index deb291deb0d5a1..609bd717f3c485 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n'; import { RenderDeps } from './application'; import { DataStart } from '../../../data/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationStart } from '../../../navigation/public'; import { DashboardConstants } from './dashboard_constants'; @@ -49,7 +49,7 @@ export interface LegacyAngularInjectedDependencies { export interface DashboardPluginStartDependencies { data: DataStart; npData: NpDataStart; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } @@ -67,7 +67,7 @@ export class DashboardPlugin implements Plugin { dataStart: DataStart; npDataStart: NpDataStart; savedObjectsClient: SavedObjectsClientContract; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } | null = null; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 5b24aa13f4f77a..4c417ed2954d3f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; export interface SavedObjectDashboard extends SavedObject { @@ -34,7 +34,7 @@ export interface SavedObjectDashboard extends SavedObject { // TODO: write a migration to rid of this, it's only around for bwc. uiStateJSON?: string; lastSavedTitle: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; destroy: () => void; refreshInterval?: RefreshInterval; getQuery(): Query; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js index 46e66177b516a4..4eb68c1bf50bcb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js @@ -58,7 +58,7 @@ describe('context app', function () { .then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).to.be(true); - expect(setParentSpy.firstCall.args[0]).to.eql(false); + expect(setParentSpy.firstCall.args[0]).to.be(undefined); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js index 2bf3da42e24e53..ea6a8c092e242b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js @@ -196,7 +196,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js index b8bec40f2859ca..486c8ed9b410eb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js @@ -199,7 +199,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js index 62bbc6166662f8..8c4cce810ca131 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js @@ -30,7 +30,7 @@ export function fetchAnchorProvider(indexPatterns) { ) { const indexPattern = await indexPatterns.get(indexPatternId); const searchSource = new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('version', true) .setField('size', 1) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts index 3314bbbf189c4b..68ccf56594e723 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../ui/public/courier'; import { IndexPatterns, IndexPattern, getServices } from '../../../kibana_services'; -import { reverseSortDir, SortDirection } from './utils/sorting'; +import { reverseSortDir } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; @@ -114,7 +115,7 @@ function fetchContextProvider(indexPatterns: IndexPatterns) { async function createSearchSource(indexPattern: IndexPattern, filters: esFilters.Filter[]) { return new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts index eeae2aa2c5d0ae..33f4454c18d400 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { reverseSortDir, SortDirection } from '../sorting'; +import { reverseSortDir } from '../sorting'; +import { SortDirection } from '../../../../../../../../../ui/public/courier'; + +jest.mock('ui/new_platform'); describe('function reverseSortDir', function() { test('reverse a given sort direction', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts index 2810e5d9d7e663..19c2ee2cdfe10f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { SearchSource } from '../../../../kibana_services'; +import { + EsQuerySortValue, + SortDirection, + SearchSourceContract, +} from '../../../../../../../../ui/public/courier'; import { convertTimeValueToIso } from './date_conversion'; -import { SortDirection } from './sorting'; import { EsHitRecordList } from '../context'; import { IntervalValue } from './generate_intervals'; -import { EsQuerySort } from './get_es_query_sort'; import { EsQuerySearchAfter } from './get_es_query_search_after'; interface RangeQuery { @@ -38,9 +40,9 @@ interface RangeQuery { * and filters set. */ export async function fetchHitsInInterval( - searchSource: SearchSource, + searchSource: SearchSourceContract, timeField: string, - sort: EsQuerySort, + sort: [EsQuerySortValue, EsQuerySortValue], sortDir: SortDirection, interval: IntervalValue[], searchAfter: EsQuerySearchAfter, diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts index a50764fe542b16..cb4878239ff920 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; +import { SortDirection } from '../../../../../../../../ui/public/courier'; export type IntervalValue = number | null; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts index c9f9b9b939f3d8..39c69112e58cb6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts @@ -16,11 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; -type EsQuerySortValue = Record; - -export type EsQuerySort = [EsQuerySortValue, EsQuerySortValue]; +import { EsQuerySortValue, SortDirection } from '../../../../../../../../ui/public/courier/types'; /** * Returns `EsQuerySort` which is used to sort records in the ES query @@ -33,6 +30,6 @@ export function getEsQuerySort( timeField: string, tieBreakerField: string, sortDir: SortDirection -): EsQuerySort { +): [EsQuerySortValue, EsQuerySortValue] { return [{ [timeField]: sortDir }, { [tieBreakerField]: sortDir }]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts index 4a0f531845f46e..47385aecb19373 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts @@ -17,13 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../../ui/public/courier'; import { IndexPattern } from '../../../../kibana_services'; -export enum SortDirection { - asc = 'asc', - desc = 'desc', -} - /** * The list of field names that are allowed for sorting, but not included in * index pattern fields. diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index ef79cda476e51b..9fee0cfc3ea00a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, TimeRange, @@ -51,7 +52,6 @@ import { getServices, IndexPattern, RequestAdapter, - SearchSource, } from '../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; @@ -92,7 +92,7 @@ export class SearchEmbeddable extends Embeddable private inspectorAdaptors: Adapters; private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: SearchSource; + private filtersSearchSource?: SearchSourceContract; private searchInstance?: JQLite; private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; @@ -194,13 +194,11 @@ export class SearchEmbeddable extends Embeddable searchScope.inspectorAdapters = this.inspectorAdaptors; const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index')); + const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) { - return; - } + if (!this.searchScope || !this.input.timeRange) return; return getTime(indexPattern, this.input.timeRange); }); @@ -241,7 +239,7 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id); + let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id!); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 1939cc70606217..ebea646a09889a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -84,6 +84,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< const queryFilter = Private(getServices().FilterBarQueryFilterProvider); try { const savedObject = await searchLoader.get(savedObjectId); + const indexPattern = savedObject.searchSource.getField('index'); return new SearchEmbeddable( { savedSearch: savedObject, @@ -92,7 +93,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< editUrl, queryFilter, editable: getServices().capabilities.discover.save as boolean, - indexPatterns: _.compact([savedObject.searchSource.getField('index')]), + indexPatterns: indexPattern ? [indexPattern] : [], }, input, this.executeTriggerActions, diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 02b08d7fa4b612..fc5f34fab75649 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -28,7 +28,6 @@ import angular from 'angular'; // just used in embeddables and discover controll import uiRoutes from 'ui/routes'; // @ts-ignore import { uiModules } from 'ui/modules'; -import { SearchSource } from 'ui/courier'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; // @ts-ignore @@ -43,6 +42,7 @@ import { wrapInI18nContext } from 'ui/i18n'; import { docTitle } from 'ui/doc_title'; // @ts-ignore import * as docViewsRegistry from 'ui/registry/doc_views'; +import { SearchSource } from '../../../../ui/public/courier'; const services = { // new plattform @@ -87,9 +87,10 @@ export { callAfterBindingsWorkaround } from 'ui/compat'; export { getRequestInspectorStats, getResponseInspectorStats, -} from 'ui/courier/utils/courier_inspector_utils'; -// @ts-ignore -export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; + hasSearchStategyForIndexPattern, + isDefaultTypeIndexPattern, + SearchSource, +} from '../../../../ui/public/courier'; // @ts-ignore export { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; // @ts-ignore @@ -115,7 +116,6 @@ export { unhashUrl } from 'ui/state_management/state_hashing'; // EXPORT types export { Vis } from 'ui/vis'; export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; -export { SearchSource } from 'ui/courier'; export { ElasticSearchHit } from 'ui/registry/doc_views_types'; export { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; export { Adapters } from 'ui/inspector/types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 873c429bf705db..7c2fb4f118915f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -21,10 +21,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/p import { IUiActionsStart } from 'src/plugins/ui_actions/public'; import { registerFeature } from './helpers/register_feature'; import './kibana_services'; -import { - Start as EmbeddableStart, - Setup as EmbeddableSetup, -} from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; /** * These are the interfaces with your public contracts. You should export these @@ -35,11 +32,11 @@ export type DiscoverSetup = void; export type DiscoverStart = void; interface DiscoverSetupPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; } interface DiscoverStartPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; } export class DiscoverPlugin implements Plugin { diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index 7d8740243ec02b..6cdd802fa2800e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -17,13 +17,14 @@ * under the License. */ -import { SearchSource } from './kibana_services'; +import { SearchSourceContract } from '../../../../ui/public/courier'; import { SortOrder } from './angular/doc_table/components/table_header/helpers'; +export { SortOrder } from './angular/doc_table/components/table_header/helpers'; export interface SavedSearch { readonly id: string; title: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; description?: string; columns: string[]; sort: SortOrder[]; diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg new file mode 100644 index 00000000000000..20694ba6e62c78 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 924f72594ad346..a2b46dab1ef33b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -25,12 +25,12 @@ import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/saved_object'; import { Vis } from 'ui/vis'; -import { SearchSource } from 'ui/courier'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { TimeRange, @@ -53,7 +53,7 @@ const getKeys = (o: T): Array => Object.keys(o) as Array< export interface VisSavedObject extends SavedObject { vis: Vis; description?: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; title: string; uiStateJSON?: string; destroy: () => void; diff --git a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js new file mode 100644 index 00000000000000..b76a9ee7c4dbe0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function activemqMetricsSpecProvider(context) { + const moduleName = 'activemq'; + return { + id: 'activemqMetrics', + name: i18n.translate('kbn.server.tutorials.activemqMetrics.nameTitle', { + defaultMessage: 'ActiveMQ metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + }), + longDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.longDescription', { + defaultMessage: 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + isBeta: true, + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.corednsMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/kibana#/discover' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-activemq.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 2d1aaa92b1e26e..f36909e59f39be 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -80,6 +80,7 @@ import { consulMetricsSpecProvider } from './consul_metrics'; import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; import { traefikMetricsSpecProvider } from './traefik_metrics'; import { awsLogsSpecProvider } from './aws_logs'; +import { activemqMetricsSpecProvider } from './activemq_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -146,4 +147,5 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(cockroachdbMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(traefikMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); } diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 34de58048b887a..9f0ad632fd5b16 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -26,6 +26,11 @@ export default function (kibana) { hidden: true, url: '/status', }, + injectDefaultVars(server) { + return { + isStatusPageAnonymous: server.config().get('status.allowAnonymous'), + }; + } } }); } diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index 3cebc730e66ded..0915f7de25b45d 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -25,8 +25,7 @@ const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startu const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); const SECOND = 1000; -// FLAKY: https://github.com/elastic/kibana/issues/51479 -describe.skip('config/deprecation warnings', function () { +describe('config/deprecation warnings', function () { this.timeout(15 * SECOND); let stdio = ''; @@ -52,9 +51,9 @@ describe.skip('config/deprecation warnings', function () { } }); - // Either time out in 10 seconds, or resolve once the line is in our buffer + // Either time out in 60 seconds, or resolve once the line is in our buffer return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 10000)), + new Promise((resolve) => setTimeout(resolve, 60000)), new Promise((resolve, reject) => { proc.stdout.on('data', (chunk) => { stdio += chunk.toString('utf8'); diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 7399f2d08508f3..9cc4e30d4252dd 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -107,7 +107,6 @@ export default class KbnServer { __internals: { hapiServer: LegacyServiceSetupDeps['core']['http']['server']; uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; - uiPluginConfigs: LegacyServiceSetupDeps['core']['plugins']['uiPluginConfigs']; elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index de1a6059774e77..d4ef203721456d 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -27,6 +27,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; import { AggType } from './agg_type'; import { FieldParamType } from './param_types/field'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; @@ -233,10 +234,10 @@ export class AggConfig { /** * Hook for pre-flight logic, see AggType#onSearchRequestStart * @param {Courier.SearchSource} searchSource - * @param {Courier.SearchRequest} searchRequest + * @param {Courier.FetchOptions} options * @return {Promise} */ - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { if (!this.type) { return Promise.resolve(); } diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 7c0245f30a1fda..2f6951891f84d0 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -33,6 +33,7 @@ import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { IndexPattern } from '../../../core_plugins/data/public'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; function removeParentAggs(obj: any) { for (const prop in obj) { @@ -301,7 +302,7 @@ export class AggConfigs { return _.find(reqAgg.getResponseAggs(), { id }); } - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { return Promise.all( // @ts-ignore this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index 89e33784fb5fb8..6ce0b9ce38ad34 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -19,16 +19,12 @@ import chrome from 'ui/chrome'; import { noop } from 'lodash'; -import { SearchSource } from 'ui/courier'; import { i18n } from '@kbn/i18n'; +import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { AggConfigOptions } from '../agg_config'; import { IBucketAggConfig } from './_bucket_agg_type'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../courier/utils/courier_inspector_utils'; import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './inline_comp_wrapper'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; diff --git a/src/legacy/ui/public/agg_types/param_types/base.ts b/src/legacy/ui/public/agg_types/param_types/base.ts index bc8ed5d485bd4e..61ef73fb62e8a3 100644 --- a/src/legacy/ui/public/agg_types/param_types/base.ts +++ b/src/legacy/ui/public/agg_types/param_types/base.ts @@ -20,7 +20,7 @@ import { AggParam } from '../'; import { AggConfigs } from '../agg_configs'; import { AggConfig } from '../../vis'; -import { SearchSource } from '../../courier'; +import { SearchSourceContract, FetchOptions } from '../../courier/types'; export class BaseParamType implements AggParam { name: string; @@ -55,8 +55,8 @@ export class BaseParamType implements AggParam { */ modifyAggConfigOnSearchRequestStart: ( aggConfig: AggConfig, - searchSource?: SearchSource, - options?: any + searchSource?: SearchSourceContract, + options?: FetchOptions ) => void; constructor(config: Record) { diff --git a/src/legacy/ui/public/courier/fetch/call_client.test.js b/src/legacy/ui/public/courier/fetch/call_client.test.ts similarity index 64% rename from src/legacy/ui/public/courier/fetch/call_client.test.js rename to src/legacy/ui/public/courier/fetch/call_client.test.ts index 463d6c59e479ec..74c87d77dd4fd0 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.test.js +++ b/src/legacy/ui/public/courier/fetch/call_client.test.ts @@ -19,61 +19,64 @@ import { callClient } from './call_client'; import { handleResponse } from './handle_response'; +import { FetchHandlers, SearchRequest, SearchStrategySearchParams } from '../types'; const mockResponses = [{}, {}]; const mockAbortFns = [jest.fn(), jest.fn()]; const mockSearchFns = [ - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0] + abort: mockAbortFns[0], })), - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1] - })) + abort: mockAbortFns[1], + })), ]; const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); jest.mock('./handle_response', () => ({ - handleResponse: jest.fn((request, response) => response) + handleResponse: jest.fn((request, response) => response), })); jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: request => mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: id => mockSearchStrategies[id] + getSearchStrategyForSearchRequest: (request: SearchRequest) => + mockSearchStrategies[request._searchStrategyId], + getSearchStrategyById: (id: number) => mockSearchStrategies[id], })); describe('callClient', () => { beforeEach(() => { - handleResponse.mockClear(); + (handleResponse as jest.Mock).mockClear(); mockAbortFns.forEach(fn => fn.mockClear()); mockSearchFns.forEach(fn => fn.mockClear()); }); test('Executes each search strategy with its group of matching requests', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; - - callClient(searchRequests); + const searchRequests = [ + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + ]; + + callClient(searchRequests, [], {} as FetchHandlers); expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([searchRequests[0], searchRequests[2]]); + expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[0], + searchRequests[2], + ]); expect(mockSearchFns[1]).toBeCalled(); - expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([searchRequests[1], searchRequests[3]]); + expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[1], + searchRequests[3], + ]); }); test('Passes the additional arguments it is given to the search strategy', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }]; - const args = { es: {}, config: {}, esShardTimeout: 0 }; + const searchRequests = [{ _searchStrategyId: 0 }]; + const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); @@ -82,25 +85,17 @@ describe('callClient', () => { }); test('Returns the responses in the original order', async () => { - const searchRequests = [{ - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }]; + const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; - const responses = await Promise.all(callClient(searchRequests)); + const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); expect(responses).toEqual([mockResponses[1], mockResponses[0]]); }); test('Calls handleResponse with each request and response', async () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - const responses = callClient(searchRequests); + const responses = callClient(searchRequests, [], {} as FetchHandlers); await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); @@ -109,17 +104,15 @@ describe('callClient', () => { }); test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; const abortController = new AbortController(); - const requestOptions = [{ - abortSignal: abortController.signal - }]; + const requestOptions = [ + { + abortSignal: abortController.signal, + }, + ]; - callClient(searchRequests, requestOptions); + callClient(searchRequests, requestOptions, {} as FetchHandlers); abortController.abort(); expect(mockAbortFns[0]).toBeCalled(); diff --git a/src/legacy/ui/public/courier/fetch/call_client.js b/src/legacy/ui/public/courier/fetch/call_client.ts similarity index 70% rename from src/legacy/ui/public/courier/fetch/call_client.js rename to src/legacy/ui/public/courier/fetch/call_client.ts index 971ae4c49a6045..43da27f941e4e2 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.js +++ b/src/legacy/ui/public/courier/fetch/call_client.ts @@ -20,11 +20,20 @@ import { groupBy } from 'lodash'; import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; import { handleResponse } from './handle_response'; +import { FetchOptions, FetchHandlers } from './types'; +import { SearchRequest } from '../types'; -export function callClient(searchRequests, requestsOptions = [], { es, config, esShardTimeout } = {}) { +export function callClient( + searchRequests: SearchRequest[], + requestsOptions: FetchOptions[] = [], + { es, config, esShardTimeout }: FetchHandlers +) { // Correlate the options with the request that they're associated with - const requestOptionEntries = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); + const requestOptionEntries: Array<[ + SearchRequest, + FetchOptions + ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); + const requestOptionsMap = new Map(requestOptionEntries); // Group the requests by the strategy used to search that specific request const searchStrategyMap = groupBy(searchRequests, (request, i) => { @@ -39,15 +48,22 @@ export function callClient(searchRequests, requestsOptions = [], { es, config, e Object.keys(searchStrategyMap).forEach(searchStrategyId => { const searchStrategy = getSearchStrategyById(searchStrategyId); const requests = searchStrategyMap[searchStrategyId]; - const { searching, abort } = searchStrategy.search({ searchRequests: requests, es, config, esShardTimeout }); + + // There's no way `searchStrategy` could be undefined here because if we didn't get a matching strategy for this ID + // then an error would have been thrown above + const { searching, abort } = searchStrategy!.search({ + searchRequests: requests, + es, + config, + esShardTimeout, + }); + requests.forEach((request, i) => { const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal } = requestOptionsMap.get(request) || {}; + const { abortSignal = null } = requestOptionsMap.get(request) || {}; if (abortSignal) abortSignal.addEventListener('abort', abort); requestResponseMap.set(request, response); }); }, []); return searchRequests.map(request => requestResponseMap.get(request)); } - - diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts index de32b9d7b30878..22fc20233cc87d 100644 --- a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts +++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts @@ -24,6 +24,7 @@ export interface Request { sort: unknown; stored_fields: string[]; } + export interface ResponseWithShardFailure { _shards: { failed: number; diff --git a/src/legacy/ui/public/courier/fetch/errors.ts b/src/legacy/ui/public/courier/fetch/errors.ts index aba554a795258d..a2ac013915b4bb 100644 --- a/src/legacy/ui/public/courier/fetch/errors.ts +++ b/src/legacy/ui/public/courier/fetch/errors.ts @@ -17,17 +17,18 @@ * under the License. */ +import { SearchError } from '../../courier'; import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { SearchResponse } from '../types'; /** * Request Failure - When an entire multi request fails * @param {Error} err - the Error that came back * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp: any; - constructor(err: any, resp?: any) { - err = err || false; - super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err.message)}`); + public resp: SearchResponse; + constructor(err: SearchError | null = null, resp?: SearchResponse) { + super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; } diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.js b/src/legacy/ui/public/courier/fetch/fetch_soon.js deleted file mode 100644 index ef02beddcb59af..00000000000000 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { callClient } from './call_client'; - -/** - * This function introduces a slight delay in the request process to allow multiple requests to queue - * up (e.g. when a dashboard is loading). - */ -export async function fetchSoon(request, options, { es, config, esShardTimeout }) { - const delay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, delay); -} - -/** - * Delays executing a function for a given amount of time, and returns a promise that resolves - * with the result. - * @param fn The function to invoke - * @param ms The number of milliseconds to wait - * @return Promise A promise that resolves with the result of executing the function - */ -function delay(fn, ms) { - return new Promise(resolve => { - setTimeout(() => resolve(fn()), ms); - }); -} - -// The current batch/queue of requests to fetch -let requestsToFetch = []; -let requestOptions = []; - -// The in-progress fetch (if there is one) -let fetchInProgress = null; - -/** - * Delay fetching for a given amount of time, while batching up the requests to be fetched. - * Returns a promise that resolves with the response for the given request. - * @param request The request to fetch - * @param ms The number of milliseconds to wait (and batch requests) - * @return Promise The response for the given request - */ -async function delayedFetch(request, options, { es, config, esShardTimeout }, ms) { - const i = requestsToFetch.length; - requestsToFetch = [...requestsToFetch, request]; - requestOptions = [...requestOptions, options]; - const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); - requestsToFetch = []; - requestOptions = []; - fetchInProgress = null; - return response; - }, ms)); - return responses[i]; -} diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts similarity index 63% rename from src/legacy/ui/public/courier/fetch/fetch_soon.test.js rename to src/legacy/ui/public/courier/fetch/fetch_soon.test.ts index 824a4ab7e12e3c..e753c526b748d9 100644 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts @@ -19,47 +19,53 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; - -function getMockConfig(config) { - const entries = Object.entries(config); - return new Map(entries); +import { UiSettingsClientContract } from '../../../../../core/public'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; } -const mockResponses = { - 'foo': {}, - 'bar': {}, - 'baz': {}, +const mockResponses: Record = { + foo: {}, + bar: {}, + baz: {}, }; jest.useFakeTimers(); jest.mock('./call_client', () => ({ - callClient: jest.fn(requests => { + callClient: jest.fn((requests: SearchRequest[]) => { // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) // in addition to how long to simulate waiting before returning a response (_waitMs) const responses = requests.map(request => { - const waitMs = requests.reduce((total, request) => request._waitMs || 0, 0); + const waitMs = requests.reduce((total, { _waitMs }) => total + _waitMs || 0, 0); return new Promise(resolve => { - resolve(mockResponses[request._mockResponseId]); - }, waitMs); + setTimeout(() => { + resolve(mockResponses[request._mockResponseId]); + }, waitMs); + }); }); return Promise.resolve(responses); - }) + }), })); describe('fetchSoon', () => { beforeEach(() => { - callClient.mockClear(); + (callClient as jest.Mock).mockClear(); }); test('should delay by 0ms if config is set to not batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -67,13 +73,13 @@ describe('fetchSoon', () => { }); test('should delay by 50ms if config is set to batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -83,30 +89,30 @@ describe('fetchSoon', () => { }); test('should send a batch of requests to callClient', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; requests.forEach((request, i) => { - fetchSoon(request, options[i], { config }); + fetchSoon(request, options[i] as FetchOptions, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(requests); - expect(callClient.mock.calls[0][1]).toEqual(options); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(requests); + expect((callClient as jest.Mock).mock.calls[0][1]).toEqual(options); }); test('should return the response to the corresponding call for multiple batched requests', async () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; const promises = requests.map(request => { - return fetchSoon(request, {}, { config }); + return fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); const results = await Promise.all(promises); @@ -115,26 +121,26 @@ describe('fetchSoon', () => { }); test('should wait for the previous batch to start before starting a new batch', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; firstBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); secondBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(firstBatch); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(firstBatch); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(2); - expect(callClient.mock.calls[1][0]).toEqual(secondBatch); + expect((callClient as jest.Mock).mock.calls[1][0]).toEqual(secondBatch); }); }); diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.ts new file mode 100644 index 00000000000000..75de85e02a1a2b --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { callClient } from './call_client'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +/** + * This function introduces a slight delay in the request process to allow multiple requests to queue + * up (e.g. when a dashboard is loading). + */ +export async function fetchSoon( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers +) { + const msToDelay = config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); +} + +/** + * Delays executing a function for a given amount of time, and returns a promise that resolves + * with the result. + * @param fn The function to invoke + * @param ms The number of milliseconds to wait + * @return Promise A promise that resolves with the result of executing the function + */ +function delay(fn: Function, ms: number) { + return new Promise(resolve => { + setTimeout(() => resolve(fn()), ms); + }); +} + +// The current batch/queue of requests to fetch +let requestsToFetch: SearchRequest[] = []; +let requestOptions: FetchOptions[] = []; + +// The in-progress fetch (if there is one) +let fetchInProgress: Promise | null = null; + +/** + * Delay fetching for a given amount of time, while batching up the requests to be fetched. + * Returns a promise that resolves with the response for the given request. + * @param request The request to fetch + * @param ms The number of milliseconds to wait (and batch requests) + * @return Promise The response for the given request + */ +async function delayedFetch( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers, + ms: number +) { + const i = requestsToFetch.length; + requestsToFetch = [...requestsToFetch, request]; + requestOptions = [...requestOptions, options]; + const responses = await (fetchInProgress = + fetchInProgress || + delay(() => { + const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + requestsToFetch = []; + requestOptions = []; + fetchInProgress = null; + return response; + }, ms)); + return responses[i]; +} diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.js b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts similarity index 96% rename from src/legacy/ui/public/courier/fetch/get_search_params.test.js rename to src/legacy/ui/public/courier/fetch/get_search_params.test.ts index 380d1da963ddf2..d6f3d33099599e 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.test.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts @@ -18,11 +18,12 @@ */ import { getMSearchParams, getSearchParams } from './get_search_params'; +import { UiSettingsClientContract } from '../../../../../core/public'; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } describe('getMSearchParams', () => { diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.js b/src/legacy/ui/public/courier/fetch/get_search_params.ts similarity index 73% rename from src/legacy/ui/public/courier/fetch/get_search_params.js rename to src/legacy/ui/public/courier/fetch/get_search_params.ts index dd55201ba5540d..6b8da07ca93d4f 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.ts @@ -17,9 +17,11 @@ * under the License. */ +import { UiSettingsClientContract } from '../../../../../core/public'; + const sessionId = Date.now(); -export function getMSearchParams(config) { +export function getMSearchParams(config: UiSettingsClientContract) { return { rest_total_hits_as_int: true, ignore_throttled: getIgnoreThrottled(config), @@ -27,7 +29,7 @@ export function getMSearchParams(config) { }; } -export function getSearchParams(config, esShardTimeout) { +export function getSearchParams(config: UiSettingsClientContract, esShardTimeout: number = 0) { return { rest_total_hits_as_int: true, ignore_unavailable: true, @@ -38,21 +40,23 @@ export function getSearchParams(config, esShardTimeout) { }; } -export function getIgnoreThrottled(config) { +export function getIgnoreThrottled(config: UiSettingsClientContract) { return !config.get('search:includeFrozen'); } -export function getMaxConcurrentShardRequests(config) { +export function getMaxConcurrentShardRequests(config: UiSettingsClientContract) { const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } -export function getPreference(config) { +export function getPreference(config: UiSettingsClientContract) { const setRequestPreference = config.get('courier:setRequestPreference'); if (setRequestPreference === 'sessionId') return sessionId; - return setRequestPreference === 'custom' ? config.get('courier:customRequestPreference') : undefined; + return setRequestPreference === 'custom' + ? config.get('courier:customRequestPreference') + : undefined; } -export function getTimeout(esShardTimeout) { +export function getTimeout(esShardTimeout: number) { return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; } diff --git a/src/legacy/ui/public/courier/fetch/handle_response.test.js b/src/legacy/ui/public/courier/fetch/handle_response.test.ts similarity index 78% rename from src/legacy/ui/public/courier/fetch/handle_response.test.js rename to src/legacy/ui/public/courier/fetch/handle_response.test.ts index 0836832e6c05a1..0163aca7771610 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.test.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.test.ts @@ -23,46 +23,50 @@ import { toastNotifications } from '../../notify/toasts'; jest.mock('../../notify/toasts', () => { return { toastNotifications: { - addWarning: jest.fn() - } + addWarning: jest.fn(), + }, }; }); jest.mock('@kbn/i18n', () => { return { i18n: { - translate: (id, { defaultMessage }) => defaultMessage - } + translate: (id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage, + }, }; }); describe('handleResponse', () => { beforeEach(() => { - toastNotifications.addWarning.mockReset(); + (toastNotifications.addWarning as jest.Mock).mockReset(); }); test('should notify if timed out', () => { const request = { body: {} }; const response = { - timed_out: true + timed_out: true, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('request timed out'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'request timed out' + ); }); test('should notify if shards failed', () => { const request = { body: {} }; const response = { _shards: { - failed: true - } + failed: true, + }, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('shards failed'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'shards failed' + ); }); test('returns the response', () => { diff --git a/src/legacy/ui/public/courier/fetch/handle_response.js b/src/legacy/ui/public/courier/fetch/handle_response.tsx similarity index 71% rename from src/legacy/ui/public/courier/fetch/handle_response.js rename to src/legacy/ui/public/courier/fetch/handle_response.tsx index fb2797369d78fa..d7f2263268f8c4 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.tsx @@ -17,14 +17,16 @@ * under the License. */ - import React from 'react'; -import { toastNotifications } from '../../notify/toasts'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; +import { toastNotifications } from '../../notify/toasts'; import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button'; +import { Request, ResponseWithShardFailure } from './components/shard_failure_types'; +import { SearchRequest, SearchResponse } from '../types'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; -export function handleResponse(request, response) { +export function handleResponse(request: SearchRequest, response: SearchResponse) { if (response.timed_out) { toastNotifications.addWarning({ title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', { @@ -41,26 +43,26 @@ export function handleResponse(request, response) { shardsTotal: response._shards.total, }, }); - const description = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationDescription', { - defaultMessage: 'The data you are seeing might be incomplete or wrong.', - }); + const description = i18n.translate( + 'common.ui.courier.fetch.shardsFailedNotificationDescription', + { + defaultMessage: 'The data you are seeing might be incomplete or wrong.', + } + ); - const text = ( + const text = toMountPoint( <> {description} - + ); - toastNotifications.addWarning({ - title, - text, - }); + toastNotifications.addWarning({ title, text }); } return response; diff --git a/src/legacy/ui/public/courier/fetch/index.js b/src/legacy/ui/public/courier/fetch/index.ts similarity index 100% rename from src/legacy/ui/public/courier/fetch/index.js rename to src/legacy/ui/public/courier/fetch/index.ts diff --git a/src/legacy/ui/public/courier/fetch/types.ts b/src/legacy/ui/public/courier/fetch/types.ts new file mode 100644 index 00000000000000..e341e1ab35c5cb --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface ApiCaller { + search: (searchRequest: SearchRequest) => ApiCallerResponse; + msearch: (searchRequest: SearchRequest) => ApiCallerResponse; +} + +export interface ApiCallerResponse extends Promise { + abort: () => void; +} + +export interface FetchOptions { + abortSignal?: AbortSignal; + searchStrategyId?: string; +} + +export interface FetchHandlers { + es: ApiCaller; + config: UiSettingsClientContract; + esShardTimeout: number; +} diff --git a/src/legacy/ui/public/courier/index.d.ts b/src/legacy/ui/public/courier/index.ts similarity index 94% rename from src/legacy/ui/public/courier/index.d.ts rename to src/legacy/ui/public/courier/index.ts index 93556c2666c9a0..3c16926d2aba70 100644 --- a/src/legacy/ui/public/courier/index.d.ts +++ b/src/legacy/ui/public/courier/index.ts @@ -17,6 +17,8 @@ * under the License. */ +export * from './fetch'; export * from './search_source'; export * from './search_strategy'; export * from './utils/courier_inspector_utils'; +export * from './types'; diff --git a/src/legacy/ui/public/courier/search_poll/search_poll.js b/src/legacy/ui/public/courier/search_poll/search_poll.js deleted file mode 100644 index f00c2a32e0ec61..00000000000000 --- a/src/legacy/ui/public/courier/search_poll/search_poll.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -import { timefilter } from 'ui/timefilter'; - -export class SearchPoll { - constructor() { - this._isPolling = false; - this._intervalInMs = undefined; - this._timerId = null; - } - - setIntervalInMs = intervalInMs => { - this._intervalInMs = _.parseInt(intervalInMs); - }; - - resume = () => { - this._isPolling = true; - this.resetTimer(); - }; - - pause = () => { - this._isPolling = false; - this.clearTimer(); - }; - - resetTimer = () => { - // Cancel the pending search and schedule a new one. - this.clearTimer(); - - if (this._isPolling) { - this._timerId = setTimeout(this._search, this._intervalInMs); - } - }; - - clearTimer = () => { - // Cancel the pending search, if there is one. - if (this._timerId) { - clearTimeout(this._timerId); - this._timerId = null; - } - }; - - _search = () => { - // Schedule another search. - this.resetTimer(); - - timefilter.notifyShouldFetch(); - }; -} diff --git a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js deleted file mode 100644 index 279e389dec114a..00000000000000 --- a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import '../../../private'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { normalizeSortRequest } from '../_normalize_sort_request'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import _ from 'lodash'; - -describe('SearchSource#normalizeSortRequest', function () { - let indexPattern; - let normalizedSort; - const defaultSortOptions = { unmapped_type: 'boolean' }; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - normalizedSort = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - })); - - it('should return an array', function () { - const sortable = { someField: 'desc' }; - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(result).to.be.an(Array); - expect(result).to.eql(normalizedSort); - // ensure object passed in is not mutated - expect(result[0]).to.not.be.equal(sortable); - expect(sortable).to.eql({ someField: 'desc' }); - }); - - it('should make plain string sort into the more verbose format', function () { - const result = normalizeSortRequest([{ someField: 'desc' }], indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should append default sort options', function () { - const sortState = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - const result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should enable script based sorting', function () { - const fieldName = 'script string'; - const direction = 'desc'; - const indexField = indexPattern.fields.getByName(fieldName); - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = { - _script: { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: indexField.type, - order: direction - } - }; - - let result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - - sortState[fieldName] = { order: direction }; - result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - }); - - it('should use script based sorting only on sortable types', function () { - const fieldName = 'script murmur3'; - const direction = 'asc'; - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = {}; - normalizedSort[fieldName] = { - order: direction, - unmapped_type: 'boolean' - }; - const result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - - expect(result).to.eql([normalizedSort]); - }); - - it('should remove unmapped_type parameter from _score sorting', function () { - const sortable = { _score: 'desc' }; - const expected = [{ - _score: { - order: 'desc' - } - }]; - - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(_.isEqual(result, expected)).to.be.ok(); - - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js deleted file mode 100644 index 3e5d7a13741156..00000000000000 --- a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -/** - * Decorate queries with default parameters - * @param {query} query object - * @returns {object} - */ -export function normalizeSortRequest(sortObject, indexPattern, defaultSortOptions) { - // [].concat({}) -> [{}], [].concat([{}]) -> [{}] - return [].concat(sortObject).map(function (sortable) { - return normalize(sortable, indexPattern, defaultSortOptions); - }); -} - -/* - Normalize the sort description to the more verbose format: - { someField: "desc" } into { someField: { "order": "desc"}} - */ -function normalize(sortable, indexPattern, defaultSortOptions) { - const normalized = {}; - let sortField = _.keys(sortable)[0]; - let sortValue = sortable[sortField]; - const indexField = indexPattern.fields.getByName(sortField); - - if (indexField && indexField.scripted && indexField.sortable) { - let direction; - if (_.isString(sortValue)) direction = sortValue; - if (_.isObject(sortValue) && sortValue.order) direction = sortValue.order; - - sortField = '_script'; - sortValue = { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: castSortType(indexField.type), - order: direction - }; - } else { - if (_.isString(sortValue)) { - sortValue = { order: sortValue }; - } - sortValue = _.defaults({}, sortValue, defaultSortOptions); - - if (sortField === '_score') { - delete sortValue.unmapped_type; - } - } - - normalized[sortField] = sortValue; - return normalized; -} - -// The ES API only supports sort scripts of type 'number' and 'string' -function castSortType(type) { - const typeCastings = { - number: 'number', - string: 'string', - date: 'number', - boolean: 'string' - }; - - const castedType = typeCastings[type]; - if (!castedType) { - throw new Error(`Unsupported script sort type: ${type}`); - } - - return castedType; -} diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts similarity index 88% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts index b220361e33b3ba..522117fe228047 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts @@ -23,11 +23,8 @@ test('Should exclude docvalue_fields that are not contained in fields', () => { const docvalueFields = [ 'my_ip_field', { field: 'my_keyword_field' }, - { field: 'my_date_field', 'format': 'epoch_millis' } + { field: 'my_date_field', format: 'epoch_millis' }, ]; const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); - expect(out).toEqual([ - 'my_ip_field', - { field: 'my_keyword_field' }, - ]); + expect(out).toEqual(['my_ip_field', { field: 'my_keyword_field' }]); }); diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts similarity index 84% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts index cd726709b4b5c2..917d26f0decd1e 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts @@ -17,7 +17,15 @@ * under the License. */ -export function filterDocvalueFields(docvalueFields, fields) { +interface DocvalueField { + field: string; + [key: string]: unknown; +} + +export function filterDocvalueFields( + docvalueFields: Array, + fields: string[] +) { return docvalueFields.filter(docValue => { const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; return fields.includes(docvalueFieldName); diff --git a/src/legacy/ui/public/courier/search_source/index.d.ts b/src/legacy/ui/public/courier/search_source/index.ts similarity index 94% rename from src/legacy/ui/public/courier/search_source/index.d.ts rename to src/legacy/ui/public/courier/search_source/index.ts index dcae7b3d2ff058..72170adc2b1296 100644 --- a/src/legacy/ui/public/courier/search_source/index.d.ts +++ b/src/legacy/ui/public/courier/search_source/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SearchSource } from './search_source'; +export * from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/mocks.ts b/src/legacy/ui/public/courier/search_source/mocks.ts index bf546c1b9e7c25..2b83f379b4f090 100644 --- a/src/legacy/ui/public/courier/search_source/mocks.ts +++ b/src/legacy/ui/public/courier/search_source/mocks.ts @@ -36,21 +36,22 @@ * under the License. */ -export const searchSourceMock = { +import { SearchSourceContract } from './search_source'; + +export const searchSourceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), - getPreferredSearchStrategyId: jest.fn(), - setFields: jest.fn(), - setField: jest.fn(), + setFields: jest.fn().mockReturnThis(), + setField: jest.fn().mockReturnThis(), getId: jest.fn(), getFields: jest.fn(), getField: jest.fn(), getOwnField: jest.fn(), - create: jest.fn(), - createCopy: jest.fn(), - createChild: jest.fn(), + create: jest.fn().mockReturnThis(), + createCopy: jest.fn().mockReturnThis(), + createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), - getParent: jest.fn(), - fetch: jest.fn(), + getParent: jest.fn().mockReturnThis(), + fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), destroy: jest.fn(), diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts new file mode 100644 index 00000000000000..d27b01eb5cf7cf --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { normalizeSortRequest } from './normalize_sort_request'; +import { SortDirection } from './types'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +jest.mock('ui/new_platform'); + +describe('SearchSource#normalizeSortRequest', function() { + const scriptedField = { + name: 'script string', + type: 'number', + scripted: true, + sortable: true, + script: 'foo', + lang: 'painless', + }; + const murmurScriptedField = { + ...scriptedField, + sortable: false, + name: 'murmur script', + type: 'murmur3', + }; + const indexPattern = { + fields: [scriptedField, murmurScriptedField], + } as IndexPattern; + + it('should return an array', function() { + const sortable = { someField: SortDirection.desc }; + const result = normalizeSortRequest(sortable, indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + // ensure object passed in is not mutated + expect(result[0]).not.toBe(sortable); + expect(sortable).toEqual({ someField: SortDirection.desc }); + }); + + it('should make plain string sort into the more verbose format', function() { + const result = normalizeSortRequest([{ someField: SortDirection.desc }], indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should append default sort options', function() { + const defaultSortOptions = { + unmapped_type: 'boolean', + }; + const result = normalizeSortRequest( + [{ someField: SortDirection.desc }], + indexPattern, + defaultSortOptions + ); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + ...defaultSortOptions, + }, + }, + ]); + }); + + it('should enable script based sorting', function() { + const result = normalizeSortRequest( + { + [scriptedField.name]: SortDirection.desc, + }, + indexPattern + ); + expect(result).toEqual([ + { + _script: { + script: { + source: scriptedField.script, + lang: scriptedField.lang, + }, + type: scriptedField.type, + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should use script based sorting only on sortable types', function() { + const result = normalizeSortRequest( + [ + { + [murmurScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + [murmurScriptedField.name]: { + order: SortDirection.asc, + }, + }, + ]); + }); + + it('should remove unmapped_type parameter from _score sorting', function() { + const result = normalizeSortRequest({ _score: SortDirection.desc }, indexPattern, { + unmapped_type: 'boolean', + }); + expect(result).toEqual([ + { + _score: { + order: SortDirection.desc, + }, + }, + ]); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts new file mode 100644 index 00000000000000..0f8fc8076caa08 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { EsQuerySortValue, SortOptions } from './types'; + +export function normalizeSortRequest( + sortObject: EsQuerySortValue | EsQuerySortValue[], + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: SortOptions = {} +) { + const sortArray: EsQuerySortValue[] = Array.isArray(sortObject) ? sortObject : [sortObject]; + return sortArray.map(function(sortable) { + return normalize(sortable, indexPattern, defaultSortOptions); + }); +} + +/** + * Normalize the sort description to the more verbose format (e.g. { someField: "desc" } into + * { someField: { "order": "desc"}}), and convert sorts on scripted fields into the proper script + * for Elasticsearch. Mix in the default options according to the advanced settings. + */ +function normalize( + sortable: EsQuerySortValue, + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: any +) { + const [[sortField, sortOrder]] = Object.entries(sortable); + const order = typeof sortOrder === 'object' ? sortOrder : { order: sortOrder }; + + if (indexPattern && typeof indexPattern !== 'string') { + const indexField = indexPattern.fields.find(({ name }) => name === sortField); + if (indexField && indexField.scripted && indexField.sortable) { + return { + _script: { + script: { + source: indexField.script, + lang: indexField.lang, + }, + type: castSortType(indexField.type), + ...order, + }, + }; + } + } + + // Don't include unmapped_type for _score field + const { unmapped_type, ...otherSortOptions } = defaultSortOptions; + return { + [sortField]: { ...order, ...(sortField === '_score' ? otherSortOptions : defaultSortOptions) }, + }; +} + +// The ES API only supports sort scripts of type 'number' and 'string' +function castSortType(type: string) { + if (['number', 'string'].includes(type)) { + return 'number'; + } else if (['string', 'boolean'].includes(type)) { + return 'string'; + } + throw new Error(`Unsupported script sort type: ${type}`); +} diff --git a/src/legacy/ui/public/courier/search_source/search_source.d.ts b/src/legacy/ui/public/courier/search_source/search_source.d.ts deleted file mode 100644 index 674e7ace0594c2..00000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export declare class SearchSource { - setPreferredSearchStrategyId: (searchStrategyId: string) => void; - getPreferredSearchStrategyId: () => string; - setFields: (newFields: any) => SearchSource; - setField: (field: string, value: any) => SearchSource; - getId: () => string; - getFields: () => any; - getField: (field: string) => any; - getOwnField: () => any; - create: () => SearchSource; - createCopy: () => SearchSource; - createChild: (options?: any) => SearchSource; - setParent: (parent: SearchSource | boolean) => SearchSource; - getParent: () => SearchSource | undefined; - fetch: (options?: any) => Promise; - onRequestStart: (handler: (searchSource: SearchSource, options: any) => void) => void; - getSearchRequestBody: () => any; - destroy: () => void; - history: any[]; -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.js b/src/legacy/ui/public/courier/search_source/search_source.js deleted file mode 100644 index bc69e862fea487..00000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.js +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * @name SearchSource - * - * @description A promise-based stream of search results that can inherit from other search sources. - * - * Because filters/queries in Kibana have different levels of persistence and come from different - * places, it is important to keep track of where filters come from for when they are saved back to - * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects - * that can have associated query parameters (index, query, filter, etc) which can also inherit from - * other searchSource objects. - * - * At query time, all of the searchSource objects that have subscribers are "flattened", at which - * point the query params from the searchSource are collected while traversing up the inheritance - * chain. At each link in the chain a decision about how to merge the query params is made until a - * single set of query parameters is created for each active searchSource (a searchSource with - * subscribers). - * - * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy - * works in Kibana. - * - * Visualize, starting from a new search: - * - * - the `savedVis.searchSource` is set as the `appSearchSource`. - * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is - * upgraded to inherit from the `rootSearchSource`. - * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so - * they will be stored directly on the `savedVis.searchSource`. - * - Any interaction with the time filter will be written to the `rootSearchSource`, so those - * filters will not be saved by the `savedVis`. - * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are - * defined on it directly, but none of the ones that it inherits from other places. - * - * Visualize, starting from an existing search: - * - * - The `savedVis` loads the `savedSearch` on which it is built. - * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as - * the `appSearchSource`. - * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. - * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the - * filters created in the visualize application and will reconnect the filters from the - * `savedSearch` at runtime to prevent losing the relationship - * - * Dashboard search sources: - * - * - Each panel in a dashboard has a search source. - * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. - * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from - * the dashboard search source. - * - When a filter is added to the search box, or via a visualization, it is written to the - * `appSearchSource`. - */ - -import _ from 'lodash'; -import angular from 'angular'; - -import { normalizeSortRequest } from './_normalize_sort_request'; - -import { fetchSoon } from '../fetch'; -import { fieldWildcardFilter } from '../../field_wildcard'; -import { getHighlightRequest, esQuery } from '../../../../../plugins/data/public'; -import { npSetup } from 'ui/new_platform'; -import chrome from '../../chrome'; -import { RequestFailure } from '../fetch/errors'; -import { filterDocvalueFields } from './filter_docvalue_fields'; - -const FIELDS = [ - 'type', - 'query', - 'filter', - 'sort', - 'highlight', - 'highlightAll', - 'aggs', - 'from', - 'searchAfter', - 'size', - 'source', - 'version', - 'fields', - 'index', -]; - -function parseInitialFields(initialFields) { - if (!initialFields) { - return {}; - } - - return typeof initialFields === 'string' ? - JSON.parse(initialFields) - : _.cloneDeep(initialFields); -} - -function isIndexPattern(val) { - return Boolean(val && typeof val.title === 'string'); -} - -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout'); -const config = npSetup.core.uiSettings; -const getConfig = (...args) => config.get(...args); -const forIp = Symbol('for which index pattern?'); - -export class SearchSource { - constructor(initialFields) { - this._id = _.uniqueId('data_source'); - - this._searchStrategyId = undefined; - this._fields = parseInitialFields(initialFields); - this._parent = undefined; - - this.history = []; - this._requestStartHandlers = []; - this._inheritOptions = {}; - } - - /***** - * PUBLIC API - *****/ - - setPreferredSearchStrategyId(searchStrategyId) { - this._searchStrategyId = searchStrategyId; - } - - getPreferredSearchStrategyId() { - return this._searchStrategyId; - } - - setFields(newFields) { - this._fields = newFields; - return this; - } - - setField(field, value) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't set field '${field}' on SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - if (field === 'index') { - const fields = this._fields; - - const hasSource = fields.source; - const sourceCameFromIp = hasSource && fields.source.hasOwnProperty(forIp); - const sourceIsForOurIp = sourceCameFromIp && fields.source[forIp] === fields.index; - if (sourceIsForOurIp) { - delete fields.source; - } - - if (value === null || value === undefined) { - delete fields.index; - return this; - } - - if (!isIndexPattern(value)) { - throw new TypeError('expected indexPattern to be an IndexPattern duck.'); - } - - fields[field] = value; - if (!fields.source) { - // imply source filtering based on the index pattern, but allow overriding - // it by simply setting another field for "source". When index is changed - fields.source = function () { - return value.getSourceFiltering(); - }; - fields.source[forIp] = value; - } - - return this; - } - - if (value == null) { - delete this._fields[field]; - return this; - } - - this._fields[field] = value; - return this; - } - - getId() { - return this._id; - } - - getFields() { - return _.clone(this._fields); - } - - /** - * Get fields from the fields - */ - getField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - let searchSource = this; - - while (searchSource) { - const value = searchSource._fields[field]; - if (value !== void 0) { - return value; - } - - searchSource = searchSource.getParent(); - } - } - - /** - * Get the field from our own fields, don't traverse up the chain - */ - getOwnField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - const value = this._fields[field]; - if (value !== void 0) { - return value; - } - } - - create() { - return new SearchSource(); - } - - createCopy() { - const json = angular.toJson(this._fields); - const newSearchSource = new SearchSource(json); - // when serializing the internal fields we lose the internal classes used in the index - // pattern, so we have to set it again to workaround this behavior - newSearchSource.setField('index', this.getField('index')); - newSearchSource.setParent(this.getParent()); - return newSearchSource; - } - - createChild(options = {}) { - const childSearchSource = new SearchSource(); - childSearchSource.setParent(this, options); - return childSearchSource; - } - - /** - * Set a searchSource that this source should inherit from - * @param {SearchSource} searchSource - the parent searchSource - * @return {this} - chainable - */ - setParent(parent, options = {}) { - this._parent = parent; - this._inheritOptions = options; - return this; - } - - /** - * Get the parent of this SearchSource - * @return {undefined|searchSource} - */ - getParent() { - return this._parent || undefined; - } - - /** - * Fetch this source and reject the returned Promise on error - * - * @async - */ - async fetch(options) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es'); - - await this.requestIsStarting(options); - - const searchRequest = await this._flatten(); - this.history = [searchRequest]; - - const response = await fetchSoon(searchRequest, { - ...(this._searchStrategyId && { searchStrategyId: this._searchStrategyId }), - ...options, - }, { es, config, esShardTimeout }); - - if (response.error) { - throw new RequestFailure(null, response); - } - - return response; - } - - /** - * Add a handler that will be notified whenever requests start - * @param {Function} handler - * @return {undefined} - */ - onRequestStart(handler) { - this._requestStartHandlers.push(handler); - } - - /** - * Called by requests of this search source when they are started - * @param {Courier.Request} request - * @param options - * @return {Promise} - */ - requestIsStarting(options) { - const handlers = [...this._requestStartHandlers]; - // If callparentStartHandlers has been set to true, we also call all - // handlers of parent search sources. - if (this._inheritOptions.callParentStartHandlers) { - let searchSource = this.getParent(); - while (searchSource) { - handlers.push(...searchSource._requestStartHandlers); - searchSource = searchSource.getParent(); - } - } - - return Promise.all(handlers.map(fn => fn(this, options))); - } - - async getSearchRequestBody() { - const searchRequest = await this._flatten(); - return searchRequest.body; - } - - /** - * Completely destroy the SearchSource. - * @return {undefined} - */ - destroy() { - this._requestStartHandlers.length = 0; - } - - /****** - * PRIVATE APIS - ******/ - - /** - * Used to merge properties into the data within ._flatten(). - * The data is passed in and modified by the function - * - * @param {object} data - the current merged data - * @param {*} val - the value at `key` - * @param {*} key - The key of `val` - * @return {undefined} - */ - _mergeProp(data, val, key) { - if (typeof val === 'function') { - const source = this; - return Promise.resolve(val(this)) - .then(function (newVal) { - return source._mergeProp(data, newVal, key); - }); - } - - if (val == null || !key || !_.isString(key)) return; - - switch (key) { - case 'filter': - const filters = Array.isArray(val) ? val : [val]; - data.filters = [...(data.filters || []), ...filters]; - return; - case 'index': - case 'type': - case 'id': - case 'highlightAll': - if (key && data[key] == null) { - data[key] = val; - } - return; - case 'searchAfter': - key = 'search_after'; - addToBody(); - break; - case 'source': - key = '_source'; - addToBody(); - break; - case 'sort': - val = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); - addToBody(); - break; - case 'query': - data.query = (data.query || []).concat(val); - break; - case 'fields': - data[key] = _.uniq([...(data[key] || []), ...val]); - break; - default: - addToBody(); - } - - /** - * Add the key and val to the body of the request - */ - function addToBody() { - data.body = data.body || {}; - // ignore if we already have a value - if (data.body[key] == null) { - data.body[key] = val; - } - } - } - - /** - * Walk the inheritance chain of a source and return it's - * flat representation (taking into account merging rules) - * @returns {Promise} - * @resolved {Object|null} - the flat data of the SearchSource - */ - _flatten() { - // the merged data of this dataSource and it's ancestors - const flatData = {}; - - // function used to write each property from each data object in the chain to flat data - const root = this; - - // start the chain at this source - let current = this; - - // call the ittr and return it's promise - return (function ittr() { - // iterate the _fields object (not array) and - // pass each key:value pair to source._mergeProp. if _mergeProp - // returns a promise, then wait for it to complete and call _mergeProp again - return Promise.all(_.map(current._fields, function ittr(value, key) { - if (value instanceof Promise) { - return value.then(function (value) { - return ittr(value, key); - }); - } - - const prom = root._mergeProp(flatData, value, key); - return prom instanceof Promise ? prom : null; - })) - .then(function () { - // move to this sources parent - const parent = current.getParent(); - // keep calling until we reach the top parent - if (parent) { - current = parent; - return ittr(); - } - }); - }()) - .then(function () { - // This is down here to prevent the circular dependency - flatData.body = flatData.body || {}; - - const computedFields = flatData.index.getComputedFields(); - - flatData.body.stored_fields = computedFields.storedFields; - flatData.body.script_fields = flatData.body.script_fields || {}; - _.extend(flatData.body.script_fields, computedFields.scriptFields); - - const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields : []; - flatData.body.docvalue_fields = flatData.body.docvalue_fields || defaultDocValueFields; - - if (flatData.body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(flatData.body._source.excludes, config.get('metaFields')); - flatData.body.docvalue_fields = flatData.body.docvalue_fields.filter( - docvalueField => filter(docvalueField.field) - ); - } - - // if we only want to search for certain fields - const fields = flatData.fields; - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - flatData.body.docvalue_fields = filterDocvalueFields(flatData.body.docvalue_fields, fields); - flatData.body.script_fields = _.pick(flatData.body.script_fields, fields); - - // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(flatData.body.script_fields)); - flatData.body.stored_fields = remainingFields; - _.set(flatData.body, '_source.includes', remainingFields); - } - - const esQueryConfigs = esQuery.getEsQueryConfig(config); - flatData.body.query = esQuery.buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs); - - if (flatData.highlightAll != null) { - if (flatData.highlightAll && flatData.body.query) { - flatData.body.highlight = getHighlightRequest(flatData.body.query, getConfig); - } - delete flatData.highlightAll; - } - - /** - * Translate a filter into a query to support es 3+ - * @param {Object} filter - The filter to translate - * @return {Object} the query version of that filter - */ - const translateToQuery = function (filter) { - if (!filter) return; - - if (filter.query) { - return filter.query; - } - - return filter; - }; - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function (id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const filters = agg.filters.filters; - - Object.keys(filters).forEach(function (filterId) { - filters[filterId] = translateToQuery(filters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - }(flatData.body.aggs || flatData.body.aggregations)); - - return flatData; - }); - } -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.js b/src/legacy/ui/public/courier/search_source/search_source.test.js deleted file mode 100644 index 800f4e4308671e..00000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.test.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SearchSource } from '../search_source'; - -jest.mock('ui/new_platform', () => ({ - npSetup: { - core: { - injectedMetadata: { - getInjectedVar: () => 0, - } - } - } -})); - -jest.mock('../fetch', () => ({ - fetchSoon: jest.fn(), -})); - -const indexPattern = { title: 'foo' }; -const indexPattern2 = { title: 'foo' }; - -describe('SearchSource', function () { - describe('#setField()', function () { - it('sets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.setField('index', 5)).toThrow(); - }); - }); - - describe('#getField()', function () { - it('gets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.getField('unacceptablePropName')).toThrow(); - }); - }); - - describe(`#setField('index')`, function () { - describe('auto-sourceFiltering', function () { - describe('new index pattern assigned', function () { - it('generates a searchSource filter', function () { - const searchSource = new SearchSource(); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('new index pattern assigned over another', function () { - it('replaces searchSource filter with new', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - const searchSourceFilter1 = searchSource.getField('source'); - searchSource.setField('index', indexPattern2); - expect(searchSource.getField('index')).toBe(indexPattern2); - expect(typeof searchSource.getField('source')).toBe('function'); - expect(searchSource.getField('source')).not.toBe(searchSourceFilter1); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('ip assigned before custom searchSource filter', function () { - it('custom searchSource filter becomes new searchSource', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - searchSource.setField('source', football); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - searchSource.setField('source', football); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - - describe('ip assigned after custom searchSource filter', function () { - it('leaves the custom filter in place', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - }); - }); - - describe('#onRequestStart()', () => { - it('should be called when starting a request', () => { - const searchSource = new SearchSource(); - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const options = {}; - searchSource.requestIsStarting(options); - expect(fn).toBeCalledWith(searchSource, options); - }); - - it('should not be called on parent searchSource', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).not.toBeCalled(); - }); - - it('should be called on parent searchSource if callParentStartHandlers is true', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent, { callParentStartHandlers: true }); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).toBeCalledWith(searchSource, options); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.ts b/src/legacy/ui/public/courier/search_source/search_source.test.ts new file mode 100644 index 00000000000000..ddd3717f55e297 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.test.ts @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchSource } from '../search_source'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +jest.mock('ui/new_platform'); + +jest.mock('../fetch', () => ({ + fetchSoon: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../chrome', () => ({ + dangerouslyGetActiveInjector: () => ({ + get: jest.fn(), + }), +})); + +const getComputedFields = () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], +}); +const mockSource = { excludes: ['foo-*'] }; +const mockSource2 = { excludes: ['bar-*'] }; +const indexPattern = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource, +} as unknown) as IndexPattern; +const indexPattern2 = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource2, +} as unknown) as IndexPattern; + +describe('SearchSource', function() { + describe('#setField()', function() { + it('sets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe('#getField()', function() { + it('gets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe(`#setField('index')`, function() { + describe('auto-sourceFiltering', function() { + describe('new index pattern assigned', function() { + it('generates a searchSource filter', async function() { + const searchSource = new SearchSource(); + expect(searchSource.getField('index')).toBe(undefined); + expect(searchSource.getField('source')).toBe(undefined); + searchSource.setField('index', indexPattern); + expect(searchSource.getField('index')).toBe(indexPattern); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + + describe('new index pattern assigned over another', function() { + it('replaces searchSource filter with new', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + expect(searchSource.getField('index')).toBe(indexPattern2); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource2); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + }); + }); + + describe('#onRequestStart()', () => { + it('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }); + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const options = {}; + await searchSource.fetch(options); + expect(fn).toBeCalledWith(searchSource, options); + }); + + it('should not be called on parent searchSource', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).not.toBeCalled(); + }); + + it('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + callParentStartHandlers: true, + }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).toBeCalledWith(searchSource, options); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.ts b/src/legacy/ui/public/courier/search_source/search_source.ts new file mode 100644 index 00000000000000..e862bb1118a74b --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.ts @@ -0,0 +1,410 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @name SearchSource + * + * @description A promise-based stream of search results that can inherit from other search sources. + * + * Because filters/queries in Kibana have different levels of persistence and come from different + * places, it is important to keep track of where filters come from for when they are saved back to + * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects + * that can have associated query parameters (index, query, filter, etc) which can also inherit from + * other searchSource objects. + * + * At query time, all of the searchSource objects that have subscribers are "flattened", at which + * point the query params from the searchSource are collected while traversing up the inheritance + * chain. At each link in the chain a decision about how to merge the query params is made until a + * single set of query parameters is created for each active searchSource (a searchSource with + * subscribers). + * + * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy + * works in Kibana. + * + * Visualize, starting from a new search: + * + * - the `savedVis.searchSource` is set as the `appSearchSource`. + * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is + * upgraded to inherit from the `rootSearchSource`. + * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so + * they will be stored directly on the `savedVis.searchSource`. + * - Any interaction with the time filter will be written to the `rootSearchSource`, so those + * filters will not be saved by the `savedVis`. + * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are + * defined on it directly, but none of the ones that it inherits from other places. + * + * Visualize, starting from an existing search: + * + * - The `savedVis` loads the `savedSearch` on which it is built. + * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as + * the `appSearchSource`. + * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. + * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the + * filters created in the visualize application and will reconnect the filters from the + * `savedSearch` at runtime to prevent losing the relationship + * + * Dashboard search sources: + * + * - Each panel in a dashboard has a search source. + * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. + * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from + * the dashboard search source. + * - When a filter is added to the search box, or via a visualization, it is written to the + * `appSearchSource`. + */ + +import _ from 'lodash'; +import { npSetup } from 'ui/new_platform'; +import { normalizeSortRequest } from './normalize_sort_request'; +import { fetchSoon } from '../fetch'; +import { fieldWildcardFilter } from '../../field_wildcard'; +import { getHighlightRequest, esFilters, esQuery } from '../../../../../plugins/data/public'; +import chrome from '../../chrome'; +import { RequestFailure } from '../fetch/errors'; +import { filterDocvalueFields } from './filter_docvalue_fields'; +import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; +import { FetchOptions, ApiCaller } from '../fetch/types'; + +const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; +const config = npSetup.core.uiSettings; + +export type SearchSourceContract = Pick; + +export class SearchSource { + private id: string = _.uniqueId('data_source'); + private searchStrategyId?: string; + private parent?: SearchSource; + private requestStartHandlers: Array< + (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + > = []; + private inheritOptions: SearchSourceOptions = {}; + public history: SearchRequest[] = []; + + constructor(private fields: SearchSourceFields = {}) {} + + /** *** + * PUBLIC API + *****/ + + setPreferredSearchStrategyId(searchStrategyId: string) { + this.searchStrategyId = searchStrategyId; + } + + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; + } + + setField(field: K, value: SearchSourceFields[K]) { + if (value == null) { + delete this.fields[field]; + } else { + this.fields[field] = value; + } + return this; + } + + getId() { + return this.id; + } + + getFields() { + return { ...this.fields }; + } + + /** + * Get fields from the fields + */ + getField(field: K, recurse = true): SearchSourceFields[K] { + if (!recurse || this.fields[field] !== void 0) { + return this.fields[field]; + } + const parent = this.getParent(); + return parent && parent.getField(field); + } + + /** + * Get the field from our own fields, don't traverse up the chain + */ + getOwnField(field: K): SearchSourceFields[K] { + return this.getField(field, false); + } + + create() { + return new SearchSource(); + } + + createCopy() { + const newSearchSource = new SearchSource(); + newSearchSource.setFields({ ...this.fields }); + // when serializing the internal fields we lose the internal classes used in the index + // pattern, so we have to set it again to workaround this behavior + newSearchSource.setField('index', this.getField('index')); + newSearchSource.setParent(this.getParent()); + return newSearchSource; + } + + createChild(options = {}) { + const childSearchSource = new SearchSource(); + childSearchSource.setParent(this, options); + return childSearchSource; + } + + /** + * Set a searchSource that this source should inherit from + * @param {SearchSource} parent - the parent searchSource + * @param {SearchSourceOptions} options - the inherit options + * @return {this} - chainable + */ + setParent(parent?: SearchSourceContract, options: SearchSourceOptions = {}) { + this.parent = parent as SearchSource; + this.inheritOptions = options; + return this; + } + + /** + * Get the parent of this SearchSource + * @return {undefined|searchSource} + */ + getParent() { + return this.parent; + } + + /** + * Fetch this source and reject the returned Promise on error + * + * @async + */ + async fetch(options: FetchOptions = {}) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const es = $injector.get('es') as ApiCaller; + + await this.requestIsStarting(options); + + const searchRequest = await this.flatten(); + this.history = [searchRequest]; + + const response = await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { es, config, esShardTimeout } + ); + + if (response.error) { + throw new RequestFailure(null, response); + } + + return response; + } + + /** + * Add a handler that will be notified whenever requests start + * @param {Function} handler + * @return {undefined} + */ + onRequestStart( + handler: (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + ) { + this.requestStartHandlers.push(handler); + } + + async getSearchRequestBody() { + const searchRequest = await this.flatten(); + return searchRequest.body; + } + + /** + * Completely destroy the SearchSource. + * @return {undefined} + */ + destroy() { + this.requestStartHandlers.length = 0; + } + + /** **** + * PRIVATE APIS + ******/ + + /** + * Called by requests of this search source when they are started + * @param {Courier.Request} request + * @param options + * @return {Promise} + */ + private requestIsStarting(options: FetchOptions = {}) { + const handlers = [...this.requestStartHandlers]; + // If callParentStartHandlers has been set to true, we also call all + // handlers of parent search sources. + if (this.inheritOptions.callParentStartHandlers) { + let searchSource = this.getParent(); + while (searchSource) { + handlers.push(...searchSource.requestStartHandlers); + searchSource = searchSource.getParent(); + } + } + + return Promise.all(handlers.map(fn => fn(this, options))); + } + + /** + * Used to merge properties into the data within ._flatten(). + * The data is passed in and modified by the function + * + * @param {object} data - the current merged data + * @param {*} val - the value at `key` + * @param {*} key - The key of `val` + * @return {undefined} + */ + private mergeProp( + data: SearchRequest, + val: SearchSourceFields[K], + key: K + ) { + val = typeof val === 'function' ? val(this) : val; + if (val == null || !key) return; + + const addToRoot = (rootKey: string, value: any) => { + data[rootKey] = value; + }; + + /** + * Add the key and val to the body of the request + */ + const addToBody = (bodyKey: string, value: any) => { + // ignore if we already have a value + if (data.body[bodyKey] == null) { + data.body[bodyKey] = value; + } + }; + + switch (key) { + case 'filter': + return addToRoot('filters', (data.filters || []).concat(val)); + case 'query': + return addToRoot(key, (data[key] || []).concat(val)); + case 'fields': + const fields = _.uniq((data[key] || []).concat(val)); + return addToRoot(key, fields); + case 'index': + case 'type': + case 'highlightAll': + return key && data[key] == null && addToRoot(key, val); + case 'searchAfter': + return addToBody('search_after', val); + case 'source': + return addToBody('_source', val); + case 'sort': + const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + return addToBody(key, sort); + default: + return addToBody(key, val); + } + } + + /** + * Walk the inheritance chain of a source and return its + * flat representation (taking into account merging rules) + * @returns {Promise} + * @resolved {Object|null} - the flat data of the SearchSource + */ + private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { + Object.entries(this.fields).forEach(([key, value]) => { + this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); + }); + if (this.parent) { + this.parent.mergeProps(root, searchRequest); + } + return searchRequest; + } + + private flatten() { + const searchRequest = this.mergeProps(); + + searchRequest.body = searchRequest.body || {}; + const { body, index, fields, query, filters, highlightAll } = searchRequest; + + const computedFields = index ? index.getComputedFields() : {}; + + body.stored_fields = computedFields.storedFields; + body.script_fields = body.script_fields || {}; + _.extend(body.script_fields, computedFields.scriptFields); + + const defaultDocValueFields = computedFields.docvalueFields + ? computedFields.docvalueFields + : []; + body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + + if (!body.hasOwnProperty('_source') && index) { + body._source = index.getSourceFiltering(); + } + + if (body._source) { + // exclude source fields for this index pattern specified by the user + const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => + filter(docvalueField.field) + ); + } + + // if we only want to search for certain fields + if (fields) { + // filter out the docvalue_fields, and script_fields to only include those that we are concerned with + body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); + body.script_fields = _.pick(body.script_fields, fields); + + // request the remaining fields from both stored_fields and _source + const remainingFields = _.difference(fields, _.keys(body.script_fields)); + body.stored_fields = remainingFields; + _.set(body, '_source.includes', remainingFields); + } + + const esQueryConfigs = esQuery.getEsQueryConfig(config); + body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); + + if (highlightAll && body.query) { + body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + delete searchRequest.highlightAll; + } + + const translateToQuery = (filter: esFilters.Filter) => filter && (filter.query || filter); + + // re-write filters within filter aggregations + (function recurse(aggBranch) { + if (!aggBranch) return; + Object.keys(aggBranch).forEach(function(id) { + const agg = aggBranch[id]; + + if (agg.filters) { + // translate filters aggregations + const { filters: aggFilters } = agg.filters; + Object.keys(aggFilters).forEach(filterId => { + aggFilters[filterId] = translateToQuery(aggFilters[filterId]); + }); + } + + recurse(agg.aggs || agg.aggregations); + }); + })(body.aggs || body.aggregations); + + return searchRequest; + } +} diff --git a/src/legacy/ui/public/courier/search_source/types.ts b/src/legacy/ui/public/courier/search_source/types.ts new file mode 100644 index 00000000000000..293f3d49596c38 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/types.ts @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { NameList } from 'elasticsearch'; +import { esFilters, Query } from '../../../../../plugins/data/public'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +export type EsQuerySearchAfter = [string | number, string | number]; + +export enum SortDirection { + asc = 'asc', + desc = 'desc', +} + +export type EsQuerySortValue = Record; + +export interface SearchSourceFields { + type?: string; + query?: Query; + filter?: + | esFilters.Filter[] + | esFilters.Filter + | (() => esFilters.Filter[] | esFilters.Filter | undefined); + sort?: EsQuerySortValue | EsQuerySortValue[]; + highlight?: any; + highlightAll?: boolean; + aggs?: any; + from?: number; + size?: number; + source?: NameList; + version?: boolean; + fields?: NameList; + index?: IndexPattern; + searchAfter?: EsQuerySearchAfter; +} + +export interface SearchSourceOptions { + callParentStartHandlers?: boolean; +} + +export { SearchSourceContract } from './search_source'; + +export interface SortOptions { + mode?: 'min' | 'max' | 'sum' | 'avg' | 'median'; + type?: 'double' | 'long' | 'date' | 'date_nanos'; + nested?: object; + unmapped_type?: string; + distance_type?: 'arc' | 'plane'; + unit?: string; + ignore_unmapped?: boolean; + _script?: object; +} + +export interface Request { + docvalue_fields: string[]; + _source: unknown; + query: unknown; + script_fields: unknown; + sort: unknown; + stored_fields: string[]; +} + +export interface ResponseWithShardFailure { + _shards: { + failed: number; + failures: ShardFailure[]; + skipped: number; + successful: number; + total: number; + }; +} + +export interface ShardFailure { + index: string; + node: string; + reason: { + caused_by: { + reason: string; + type: string; + }; + reason: string; + lang?: string; + script?: string; + script_stack?: string[]; + type: string; + }; + shard: number; +} + +export type SearchRequest = any; +export type SearchResponse = any; diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts similarity index 67% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts index a1ea53e8b5b477..29921fc7a11d3b 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts @@ -18,26 +18,28 @@ */ import { defaultSearchStrategy } from './default_search_strategy'; +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchStrategySearchParams } from './types'; const { search } = defaultSearchStrategy; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } -const msearchMockResponse = Promise.resolve([]); +const msearchMockResponse: any = Promise.resolve([]); msearchMockResponse.abort = jest.fn(); const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); -const searchMockResponse = Promise.resolve([]); +const searchMockResponse: any = Promise.resolve([]); searchMockResponse.abort = jest.fn(); const searchMock = jest.fn().mockReturnValue(searchMockResponse); -describe('defaultSearchStrategy', function () { - describe('search', function () { - let searchArgs; +describe('defaultSearchStrategy', function() { + describe('search', function() { + let searchArgs: MockedKeys>; beforeEach(() => { msearchMockResponse.abort.mockClear(); @@ -47,9 +49,12 @@ describe('defaultSearchStrategy', function () { searchMock.mockClear(); searchArgs = { - searchRequests: [{ - index: { title: 'foo' } - }], + searchRequests: [ + { + index: { title: 'foo' }, + }, + ], + esShardTimeout: 0, es: { msearch: msearchMock, search: searchMock, @@ -58,48 +63,48 @@ describe('defaultSearchStrategy', function () { }); test('does not send max_concurrent_shard_requests by default', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'courier:maxConcurrentShardRequests': 42, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'search:includeFrozen': true, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); test('should properly call abort with msearch', () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(msearchMockResponse.abort).toHaveBeenCalled(); }); test('should properly abort with search', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(searchMockResponse.abort).toHaveBeenCalled(); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts index 42a9b641364542..5be4fef0766555 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts @@ -17,37 +17,39 @@ * under the License. */ +import { SearchStrategyProvider, SearchStrategySearchParams } from './types'; import { addSearchStrategy } from './search_strategy_registry'; import { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; -import { getSearchParams, getMSearchParams, getPreference, getTimeout } from '../fetch/get_search_params'; +import { + getSearchParams, + getMSearchParams, + getPreference, + getTimeout, +} from '../fetch/get_search_params'; -export const defaultSearchStrategy = { +export const defaultSearchStrategy: SearchStrategyProvider = { id: 'default', search: params => { return params.config.get('courier:batchSearches') ? msearch(params) : search(params); }, - isViable: (indexPattern) => { - if (!indexPattern) { - return false; - } - - return isDefaultTypeIndexPattern(indexPattern); + isViable: indexPattern => { + return indexPattern && isDefaultTypeIndexPattern(indexPattern); }, }; -function msearch({ searchRequests, es, config, esShardTimeout }) { +function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, search_type: searchType, ignore_unavailable: true, - preference: getPreference(config) + preference: getPreference(config), }; const inlineBody = { ...body, - timeout: getTimeout(esShardTimeout) + timeout: getTimeout(esShardTimeout), }; return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; }); @@ -58,11 +60,11 @@ function msearch({ searchRequests, es, config, esShardTimeout }) { }); return { searching: searching.then(({ responses }) => responses), - abort: searching.abort + abort: searching.abort, }; } -function search({ searchRequests, es, config, esShardTimeout }) { +function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); const promises = searchRequests.map(({ index, body }) => { diff --git a/src/legacy/ui/public/courier/search_strategy/index.js b/src/legacy/ui/public/courier/search_strategy/index.ts similarity index 100% rename from src/legacy/ui/public/courier/search_strategy/index.js rename to src/legacy/ui/public/courier/search_strategy/index.ts diff --git a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts similarity index 85% rename from src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js rename to src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts index 94c85c0e13ec70..3785ce63410787 100644 --- a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js +++ b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts @@ -17,7 +17,9 @@ * under the License. */ -export const isDefaultTypeIndexPattern = indexPattern => { +import { IndexPattern } from '../../../../core_plugins/data/public'; + +export const isDefaultTypeIndexPattern = (indexPattern: IndexPattern) => { // Default index patterns don't have `type` defined. return !indexPattern.type; }; diff --git a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts similarity index 79% rename from src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts index c4499cc870d566..24c3876cfcc051 100644 --- a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts @@ -17,21 +17,25 @@ * under the License. */ -import { SearchError } from './search_error'; import { i18n } from '@kbn/i18n'; +import { SearchError } from './search_error'; +import { SearchStrategyProvider } from './types'; -export const noOpSearchStrategy = { +export const noOpSearchStrategy: SearchStrategyProvider = { id: 'noOp', - search: async () => { + search: () => { const searchError = new SearchError({ status: '418', // "I'm a teapot" error title: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageTitle', { defaultMessage: 'No search strategy registered', }), - message: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', { - defaultMessage: `Couldn't find a search strategy for the search request`, - }), + message: i18n.translate( + 'common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', + { + defaultMessage: `Couldn't find a search strategy for the search request`, + } + ), type: 'NO_OP_SEARCH_STRATEGY', path: '', }); @@ -39,7 +43,6 @@ export const noOpSearchStrategy = { return { searching: Promise.reject(searchError), abort: () => {}, - failedSearchRequests: [], }; }, diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts b/src/legacy/ui/public/courier/search_strategy/search_error.d.ts deleted file mode 100644 index bf49853957c758..00000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type SearchError = any; -export type getSearchErrorType = any; diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.js b/src/legacy/ui/public/courier/search_strategy/search_error.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/search_error.js rename to src/legacy/ui/public/courier/search_strategy/search_error.ts index 9c35d11a6abf44..d4042fb17499cb 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_error.js +++ b/src/legacy/ui/public/courier/search_strategy/search_error.ts @@ -17,8 +17,23 @@ * under the License. */ +interface SearchErrorOptions { + status: string; + title: string; + message: string; + path: string; + type: string; +} + export class SearchError extends Error { - constructor({ status, title, message, path, type }) { + public name: string; + public status: string; + public title: string; + public message: string; + public path: string; + public type: string; + + constructor({ status, title, message, path, type }: SearchErrorOptions) { super(message); this.name = 'SearchError'; this.status = status; @@ -39,9 +54,9 @@ export class SearchError extends Error { } } -export function getSearchErrorType({ message }) { +export function getSearchErrorType({ message }: Pick) { const msg = message.toLowerCase(); - if(msg.indexOf('unsupported query') > -1) { + if (msg.indexOf('unsupported query') > -1) { return 'UNSUPPORTED_QUERY'; } } diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts similarity index 58% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts index 362d303eb62034..ae2ed6128c8ea8 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; import { noOpSearchStrategy } from './no_op_search_strategy'; import { searchStrategies, @@ -24,16 +25,28 @@ import { getSearchStrategyByViability, getSearchStrategyById, getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern + hasSearchStategyForIndexPattern, } from './search_strategy_registry'; - -const mockSearchStrategies = [{ - id: 0, - isViable: index => index === 0 -}, { - id: 1, - isViable: index => index === 1 -}]; +import { SearchStrategyProvider } from './types'; + +const mockSearchStrategies: SearchStrategyProvider[] = [ + { + id: '0', + isViable: (index: IndexPattern) => index.id === '0', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, + { + id: '1', + isViable: (index: IndexPattern) => index.id === '1', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, +]; describe('Search strategy registry', () => { beforeEach(() => { @@ -59,12 +72,16 @@ describe('Search strategy registry', () => { }); it('returns the viable strategy', () => { - expect(getSearchStrategyByViability(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyByViability(1)).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyByViability({ id: '0' } as IndexPattern)).toBe( + mockSearchStrategies[0] + ); + expect(getSearchStrategyByViability({ id: '1' } as IndexPattern)).toBe( + mockSearchStrategies[1] + ); }); it('returns undefined if there is no viable strategy', () => { - expect(getSearchStrategyByViability(-1)).toBe(undefined); + expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); }); }); @@ -74,12 +91,16 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID', () => { - expect(getSearchStrategyById(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById(1)).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyById('0')).toBe(mockSearchStrategies[0]); + expect(getSearchStrategyById('1')).toBe(mockSearchStrategies[1]); }); it('returns undefined if there is no strategy with that ID', () => { - expect(getSearchStrategyById(-1)).toBe(undefined); + expect(getSearchStrategyById('-1')).toBe(undefined); + }); + + it('returns the noOp search strategy if passed that ID', () => { + expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); }); }); @@ -89,15 +110,29 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: 1 })).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: '1' })).toBe( + mockSearchStrategies[1] + ); + }); + + it('throws if there is no strategy by provided ID', () => { + expect(() => + getSearchStrategyForSearchRequest({}, { searchStrategyId: '-1' }) + ).toThrowErrorMatchingInlineSnapshot(`"No strategy with ID -1"`); }); it('returns the strategy by viability if there is one', () => { - expect(getSearchStrategyForSearchRequest({ index: 1 })).toBe(mockSearchStrategies[1]); + expect( + getSearchStrategyForSearchRequest({ + index: { + id: '1', + }, + }) + ).toBe(mockSearchStrategies[1]); }); it('returns the no op strategy if there is no viable strategy', () => { - expect(getSearchStrategyForSearchRequest({ index: 3 })).toBe(noOpSearchStrategy); + expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); }); }); @@ -107,8 +142,8 @@ describe('Search strategy registry', () => { }); it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern(0)).toBe(true); - expect(hasSearchStategyForIndexPattern(-1)).toBe(false); + expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); + expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); }); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts similarity index 64% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts index e67d39ea27aa6d..9ef007f97531ea 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts @@ -17,11 +17,14 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { SearchStrategyProvider } from './types'; import { noOpSearchStrategy } from './no_op_search_strategy'; +import { SearchResponse } from '../types'; -export const searchStrategies = []; +export const searchStrategies: SearchStrategyProvider[] = []; -export const addSearchStrategy = searchStrategy => { +export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { if (searchStrategies.includes(searchStrategy)) { return; } @@ -29,22 +32,27 @@ export const addSearchStrategy = searchStrategy => { searchStrategies.push(searchStrategy); }; -export const getSearchStrategyByViability = indexPattern => { +export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { return searchStrategies.find(searchStrategy => { return searchStrategy.isViable(indexPattern); }); }; -export const getSearchStrategyById = searchStrategyId => { - return searchStrategies.find(searchStrategy => { +export const getSearchStrategyById = (searchStrategyId: string) => { + return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { return searchStrategy.id === searchStrategyId; }); }; -export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrategyId } = {}) => { +export const getSearchStrategyForSearchRequest = ( + searchRequest: SearchResponse, + { searchStrategyId }: { searchStrategyId?: string } = {} +) => { // Allow the searchSource to declare the correct strategy with which to execute its searches. if (searchStrategyId != null) { - return getSearchStrategyById(searchStrategyId); + const strategy = getSearchStrategyById(searchStrategyId); + if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); + return strategy; } // Otherwise try to match it to a strategy. @@ -58,6 +66,6 @@ export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrateg return noOpSearchStrategy; }; -export const hasSearchStategyForIndexPattern = indexPattern => { +export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { return Boolean(getSearchStrategyByViability(indexPattern)); }; diff --git a/src/legacy/ui/public/courier/search_strategy/types.ts b/src/legacy/ui/public/courier/search_strategy/types.ts new file mode 100644 index 00000000000000..1542f9824a5b1b --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { FetchHandlers } from '../fetch/types'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface SearchStrategyProvider { + id: string; + search: (params: SearchStrategySearchParams) => SearchStrategyResponse; + isViable: (indexPattern: IndexPattern) => boolean; +} + +export interface SearchStrategyResponse { + searching: Promise; + abort: () => void; +} + +export interface SearchStrategySearchParams extends FetchHandlers { + searchRequests: SearchRequest[]; +} diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/ui/public/courier/types.ts similarity index 84% rename from src/legacy/core_plugins/data/public/filter/index.tsx rename to src/legacy/ui/public/courier/types.ts index e48a18fc53a76e..23d74ce6a57da8 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/legacy/ui/public/courier/types.ts @@ -17,4 +17,7 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters'; +export * from './fetch/types'; +export * from './search_source/types'; +export * from './search_strategy/types'; +export * from './utils/types'; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts similarity index 78% rename from src/legacy/ui/public/courier/utils/courier_inspector_utils.js rename to src/legacy/ui/public/courier/utils/courier_inspector_utils.ts index 0e53f92bd9dcbd..2c47fae4cce37a 100644 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js +++ b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts @@ -25,51 +25,57 @@ */ import { i18n } from '@kbn/i18n'; +import { SearchResponse } from 'elasticsearch'; +import { SearchSourceContract, RequestInspectorStats } from '../types'; -function getRequestInspectorStats(searchSource) { - const stats = {}; +function getRequestInspectorStats(searchSource: SearchSourceContract) { + const stats: RequestInspectorStats = {}; const index = searchSource.getField('index'); if (index) { stats.indexPattern = { label: i18n.translate('common.ui.courier.indexPatternLabel', { - defaultMessage: 'Index pattern' + defaultMessage: 'Index pattern', }), value: index.title, description: i18n.translate('common.ui.courier.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.' + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', }), }; stats.indexPatternId = { label: i18n.translate('common.ui.courier.indexPatternIdLabel', { - defaultMessage: 'Index pattern ID' + defaultMessage: 'Index pattern ID', }), - value: index.id, + value: index.id!, description: i18n.translate('common.ui.courier.indexPatternIdDescription', { defaultMessage: 'The ID in the {kibanaIndexPattern} index.', - values: { kibanaIndexPattern: '.kibana' } + values: { kibanaIndexPattern: '.kibana' }, }), }; } return stats; } -function getResponseInspectorStats(searchSource, resp) { +function getResponseInspectorStats( + searchSource: SearchSourceContract, + resp: SearchResponse +) { const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; - const stats = {}; + const stats: RequestInspectorStats = {}; if (resp && resp.took) { stats.queryTime = { label: i18n.translate('common.ui.courier.queryTimeLabel', { - defaultMessage: 'Query time' + defaultMessage: 'Query time', }), value: i18n.translate('common.ui.courier.queryTimeValue', { defaultMessage: '{queryTime}ms', values: { queryTime: resp.took }, }), description: i18n.translate('common.ui.courier.queryTimeDescription', { - defaultMessage: 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.' + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', }), }; } @@ -77,21 +83,21 @@ function getResponseInspectorStats(searchSource, resp) { if (resp && resp.hits) { stats.hitsTotal = { label: i18n.translate('common.ui.courier.hitsTotalLabel', { - defaultMessage: 'Hits (total)' + defaultMessage: 'Hits (total)', }), value: `${resp.hits.total}`, description: i18n.translate('common.ui.courier.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.' + defaultMessage: 'The number of documents that match the query.', }), }; stats.hits = { label: i18n.translate('common.ui.courier.hitsLabel', { - defaultMessage: 'Hits' + defaultMessage: 'Hits', }), value: `${resp.hits.hits.length}`, description: i18n.translate('common.ui.courier.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.' + defaultMessage: 'The number of documents returned by the query.', }), }; } @@ -99,15 +105,16 @@ function getResponseInspectorStats(searchSource, resp) { if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) { stats.requestTime = { label: i18n.translate('common.ui.courier.requestTimeLabel', { - defaultMessage: 'Request time' + defaultMessage: 'Request time', }), value: i18n.translate('common.ui.courier.requestTimeValue', { defaultMessage: '{requestTime}ms', values: { requestTime: lastRequest.ms }, }), description: i18n.translate('common.ui.courier.requestTimeDescription', { - defaultMessage: 'The time of the request from the browser to Elasticsearch and back. ' + - 'Does not include the time the requested waited in the queue.' + defaultMessage: + 'The time of the request from the browser to Elasticsearch and back. ' + + 'Does not include the time the requested waited in the queue.', }), }; } diff --git a/src/legacy/ui/public/courier/utils/types.ts b/src/legacy/ui/public/courier/utils/types.ts new file mode 100644 index 00000000000000..305f27a86b398c --- /dev/null +++ b/src/legacy/ui/public/courier/utils/types.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface InspectorStat { + label: string; + value: string; + description: string; +} + +export interface RequestInspectorStats { + indexPattern?: InspectorStat; + indexPatternId?: InspectorStat; + queryTime?: InspectorStat; + hitsTotal?: InspectorStat; + hits?: InspectorStat; + requestTime?: InspectorStat; +} diff --git a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js b/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js deleted file mode 100644 index a15c602b7ba836..00000000000000 --- a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { fieldWildcardFilter, makeRegEx } from '../../field_wildcard'; - -describe('fieldWildcard', function () { - const metaFields = ['_id', '_type', '_source']; - - beforeEach(ngMock.module('kibana')); - - describe('makeRegEx', function () { - it('matches * in any position', function () { - expect('aaaaaabbbbbbbcccccc').to.match(makeRegEx('*a*b*c*')); - expect('a1234').to.match(makeRegEx('*1234')); - expect('1234a').to.match(makeRegEx('1234*')); - expect('12a34').to.match(makeRegEx('12a34')); - }); - - it('properly escapes regexp control characters', function () { - expect('account[user_id]').to.match(makeRegEx('account[*]')); - }); - - it('properly limits matches without wildcards', function () { - expect('username').to.match(makeRegEx('*name')); - expect('username').to.match(makeRegEx('user*')); - expect('username').to.match(makeRegEx('username')); - expect('username').to.not.match(makeRegEx('user')); - expect('username').to.not.match(makeRegEx('name')); - expect('username').to.not.match(makeRegEx('erna')); - }); - }); - - describe('filter', function () { - it('filters nothing when given undefined', function () { - const filter = fieldWildcardFilter(); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('filters nothing when given an empty array', function () { - const filter = fieldWildcardFilter([], metaFields); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('does not filter metaFields', function () { - const filter = fieldWildcardFilter([ '_*' ], metaFields); - - const original = [ - '_id', - '_type', - '_typefake' - ]; - - expect(original.filter(filter)).to.eql(['_id', '_type']); - }); - - it('filters values that match the globs', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4' - ], metaFields); - - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(['bar', 'baz']); - }); - - it('handles weird values okay', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4', - 'undefined' - ], metaFields); - - const original = [ - 'foo', - null, - 'bar', - undefined, - {}, - [], - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql([null, 'bar', {}, [], 'baz']); - }); - }); -}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts new file mode 100644 index 00000000000000..9f7523866fdc19 --- /dev/null +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fieldWildcardFilter, makeRegEx } from './field_wildcard'; + +describe('fieldWildcard', () => { + const metaFields = ['_id', '_type', '_source']; + + describe('makeRegEx', function() { + it('matches * in any position', function() { + expect('aaaaaabbbbbbbcccccc').toMatch(makeRegEx('*a*b*c*')); + expect('a1234').toMatch(makeRegEx('*1234')); + expect('1234a').toMatch(makeRegEx('1234*')); + expect('12a34').toMatch(makeRegEx('12a34')); + }); + + it('properly escapes regexp control characters', function() { + expect('account[user_id]').toMatch(makeRegEx('account[*]')); + }); + + it('properly limits matches without wildcards', function() { + expect('username').toMatch(makeRegEx('*name')); + expect('username').toMatch(makeRegEx('user*')); + expect('username').toMatch(makeRegEx('username')); + expect('username').not.toMatch(makeRegEx('user')); + expect('username').not.toMatch(makeRegEx('name')); + expect('username').not.toMatch(makeRegEx('erna')); + }); + }); + + describe('filter', function() { + it('filters nothing when given undefined', function() { + const filter = fieldWildcardFilter(); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(val => filter(val))).toEqual(original); + }); + + it('filters nothing when given an empty array', function() { + const filter = fieldWildcardFilter([], metaFields); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(original); + }); + + it('does not filter metaFields', function() { + const filter = fieldWildcardFilter(['_*'], metaFields); + + const original = ['_id', '_type', '_typefake']; + + expect(original.filter(filter)).toEqual(['_id', '_type']); + }); + + it('filters values that match the globs', function() { + const filter = fieldWildcardFilter(['f*', '*4'], metaFields); + + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(['bar', 'baz']); + }); + + it('handles weird values okay', function() { + const filter = fieldWildcardFilter(['f*', '*4', 'undefined'], metaFields); + + const original = ['foo', null, 'bar', undefined, {}, [], 'baz', 1234]; + + expect(original.filter(filter)).toEqual([null, 'bar', {}, [], 'baz']); + }); + }); +}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.js b/src/legacy/ui/public/field_wildcard/field_wildcard.ts similarity index 70% rename from src/legacy/ui/public/field_wildcard/field_wildcard.js rename to src/legacy/ui/public/field_wildcard/field_wildcard.ts index 656641b20a98ca..5437086ddd6f42 100644 --- a/src/legacy/ui/public/field_wildcard/field_wildcard.js +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.ts @@ -19,25 +19,29 @@ import { escapeRegExp, memoize } from 'lodash'; -export const makeRegEx = memoize(function makeRegEx(glob) { - return new RegExp('^' + glob.split('*').map(escapeRegExp).join('.*') + '$'); +export const makeRegEx = memoize(function makeRegEx(glob: string) { + const globRegex = glob + .split('*') + .map(escapeRegExp) + .join('.*'); + return new RegExp(`^${globRegex}$`); }); // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardMatcher(globs = [], metaFields) { - return function matcher(val) { +export function fieldWildcardMatcher(globs: string[] = [], metaFields: unknown[] = []) { + return function matcher(val: unknown) { // do not test metaFields or keyword if (metaFields.indexOf(val) !== -1) { return false; } - return globs.some(p => makeRegEx(p).test(val)); + return globs.some(p => makeRegEx(p).test(`${val}`)); }; } // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardFilter(globs = [], metaFields = []) { +export function fieldWildcardFilter(globs: string[] = [], metaFields: string[] = []) { const matcher = fieldWildcardMatcher(globs, metaFields); - return function filter(val) { + return function filter(val: unknown) { return !matcher(val); }; } diff --git a/src/legacy/ui/public/field_wildcard/index.js b/src/legacy/ui/public/field_wildcard/index.ts similarity index 100% rename from src/legacy/ui/public/field_wildcard/index.js rename to src/legacy/ui/public/field_wildcard/index.ts diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index f51ae86b5c9a78..145045a90ade8f 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -35,7 +35,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index 690a9cffaa1388..d0ff0aaa8c72c7 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -30,7 +30,6 @@ export const { FieldList, // only used in Discover and StubIndexPattern flattenHitWrapper, formatHitProvider, - IndexPatternSelect, // only used in x-pack/plugin/maps and input control vis } = data.indexPatterns; // static code diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 773d4283cad881..ff89ef69d53cad 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -26,6 +26,10 @@ const mockObservable = () => { }; }; +const mockComponent = () => { + return null; +}; + export const mockUiSettings = { get: (item) => { return mockUiSettings[item]; @@ -139,6 +143,9 @@ export const npStart = { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + ui: { + IndexPatternSelect: mockComponent, + }, query: { filterManager: { getFetches$: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index cbdaccd65f94bb..e5d5cd0a877764 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -18,13 +18,15 @@ */ import { setRootControllerMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__ } from './new_platform'; +import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { coreMock } from '../../../../core/public/mocks'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { beforeEach(() => { setRootControllerMock.mockReset(); __reset__(); + __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); }); const registerApp = () => { @@ -59,7 +61,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], - appBasePath: '', + appBasePath: '/test/base/path/app/test', }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index acf1191852dc8f..c0b2d6d9132578 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -19,7 +19,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; -import { Start as EmbeddableStart, Setup as EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -35,7 +35,7 @@ import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/pu export interface PluginsSetup { data: ReturnType; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -47,7 +47,7 @@ export interface PluginsSetup { export interface PluginsStart { data: ReturnType; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; eui_utils: EuiUtilsStart; expressions: ReturnType; home: HomePublicPluginStart; @@ -111,7 +111,10 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const unmount = await app.mount( + { core: npStart.core }, + { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } + ); $scope.$on('$destroy', () => { unmount(); }); diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts index 8ef97c0b3ebccc..3d435f2ba8dfdf 100644 --- a/src/legacy/ui/public/promises/defer.ts +++ b/src/legacy/ui/public/promises/defer.ts @@ -17,7 +17,7 @@ * under the License. */ -interface Defer { +export interface Defer { promise: Promise; resolve(value: T): void; reject(reason: Error): void; diff --git a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx index ebbe886b3650bd..19cbbf9cea04c5 100644 --- a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx +++ b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx @@ -19,7 +19,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { SearchError } from 'ui/courier'; +import { SearchError } from '../../courier'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; interface VisualizationRequestErrorProps { @@ -32,7 +32,7 @@ export class VisualizationRequestError extends React.Component diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index 70e0c1f1382fad..608a8b9ce8aa7f 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -28,7 +28,7 @@ import { } from './build_pipeline'; import { Vis, VisState } from 'ui/vis'; import { AggConfig } from 'ui/agg_types/agg_config'; -import { searchSourceMock } from 'ui/courier/search_source/mocks'; +import { searchSourceMock } from '../../../courier/search_source/mocks'; jest.mock('ui/new_platform'); jest.mock('ui/agg_types/buckets/date_histogram', () => ({ diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 21b13abea440e1..ca9540b4d37370 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -20,11 +20,11 @@ import { cloneDeep, get } from 'lodash'; // @ts-ignore import { setBounds } from 'ui/agg_types'; -import { SearchSource } from 'ui/courier'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; import { isDateHistogramBucketAggConfig } from 'ui/agg_types/buckets/date_histogram'; import moment from 'moment'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../courier/types'; import { createFormat } from './utilities'; interface SchemaConfigParams { @@ -462,7 +462,7 @@ export const buildVislibDimensions = async ( // take a Vis object and decorate it with the necessary params (dimensions, bucket, metric, etc) export const getVisParams = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any; abortSignal?: AbortSignal } + params: { searchSource: SearchSourceContract; timeRange?: any; abortSignal?: AbortSignal } ) => { const schemas = getSchemas(vis, params.timeRange); let visConfig = cloneDeep(vis.params); @@ -479,7 +479,10 @@ export const getVisParams = async ( export const buildPipeline = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any } + params: { + searchSource: SearchSourceContract; + timeRange?: any; + } ) => { const { searchSource } = params; const { indexPattern } = vis; diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 36759551a17236..a9203415321faa 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -24,13 +24,13 @@ import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../../../vis'; +import { SearchSource, SearchSourceContract } from '../../../courier'; import { esFilters, Query } from '../../../../../../plugins/data/public'; -import { SearchSource } from '../../../courier'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; query?: Query; - searchSource?: SearchSource; + searchSource?: SearchSourceContract; } /** diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index c0885cd5d3d13f..763167c6b5ccf4 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -234,7 +234,7 @@ export function uiRenderMixin(kbnServer, server, config) { // Get the list of new platform plugins. // Convert the Map into an array of objects so it is JSON serializable and order is preserved. - const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPluginConfigs; + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.browserConfigs; const uiPlugins = await Promise.all([ ...kbnServer.newPlatform.__internals.uiPlugins.public.entries(), ].map(async ([id, plugin]) => { diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 28eb448d12d82b..2eaf4c1d6e8828 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -61,7 +61,7 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.discoveredPlugins = opts.discoveredPlugins; + this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -551,9 +551,9 @@ export default class BaseOptimizer { _getDiscoveredPluginEntryPoints() { // New platform plugin entry points - return [...this.discoveredPlugins.entries()] - .reduce((entryPoints, [pluginId, plugin]) => { - entryPoints[`plugin/${pluginId}`] = `${plugin.path}/public`; + return [...this.newPlatformPluginInfo.entries()] + .reduce((entryPoints, [pluginId, pluginInfo]) => { + entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; return entryPoints; }, {}); } diff --git a/src/optimize/index.js b/src/optimize/index.js index 9789e7abc2f9d2..0960f9ecb10b6d 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -66,7 +66,7 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - discoveredPlugins: newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index 16be840b3ca0eb..9fbeceb578615d 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -30,7 +30,7 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - discoveredPlugins: kbnServer.newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx index b30733760bbdfb..f15d538703e21c 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - Start as EmbeddableStart, + IEmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index f6d2fcbcd57fd4..78ce6bdc4c58f8 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, Start as EmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements IAction { private core: CoreStart, private savedobjectfinder: React.ComponentType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ActionContext) { diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx index 36efd0bcba676a..36313353e3c332 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx @@ -20,15 +20,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; -import { - IContainer, - IEmbeddable, - EmbeddableInput, - EmbeddableOutput, - Start as EmbeddableStart, -} from '../embeddable_plugin'; +import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; interface Props { container: IContainer; @@ -36,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: GetEmbeddableFactories; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 6cefd11c912f19..684aa93779bc13 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - Start as EmbeddableStartContract, + IEmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: EmbeddableStartContract; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index dbb5a06da9cd94..79cc9b6980545e 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { IUiActionsSetup, IUiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -34,12 +34,12 @@ import { } from '../../../plugins/kibana_react/public'; interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; uiActions: IUiActionsStart; } diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6a5c7bdf8eea3d..6e03c665290ae9 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -49,4 +49,11 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); + + it('should return true if the filter has meta without a key', () => { + const filter = { meta: { index: 'foo' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); }); diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 496aab3ea585f8..9b68f5088c4479 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -26,7 +26,7 @@ import { Filter } from '../filters'; * change. */ export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { - if (!filter.meta || !indexPattern) { + if (!filter.meta?.key || !indexPattern) { return true; } return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index fa07b3e611fa7c..3d819bd145fa63 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -63,18 +63,22 @@ export type RangeFilterMeta = FilterMeta & { formattedValue?: string; }; -export type RangeFilter = Filter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: string; - source: any; +export interface EsRangeFilter { + range: { [key: string]: RangeFilterParams }; +} + +export type RangeFilter = Filter & + EsRangeFilter & { + meta: RangeFilterMeta; + script?: { + script: { + params: any; + lang: string; + source: any; + }; }; + match_all?: any; }; - match_all?: any; - range: { [key: string]: RangeFilterParams }; -}; export const isRangeFilter = (filter: any): filter is RangeFilter => filter && filter.range; diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts index 5312f1be6c26c3..8788d4b690aba5 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts @@ -20,36 +20,19 @@ import { getHighlightRequest } from './highlight_request'; describe('getHighlightRequest', () => { - let configMock: Record; - const getConfig = (key: string) => configMock[key]; const queryStringQuery = { query_string: { query: 'foo' } }; - beforeEach(function() { - configMock = {}; - configMock['doc_table:highlight'] = true; - }); - test('should be a function', () => { expect(getHighlightRequest).toBeInstanceOf(Function); }); test('should not modify the original query', () => { - getHighlightRequest(queryStringQuery, getConfig); + getHighlightRequest(queryStringQuery, true); expect(queryStringQuery.query_string).not.toHaveProperty('highlight'); }); test('should return undefined if highlighting is turned off', () => { - configMock['doc_table:highlight'] = false; - const request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).toBe(undefined); - }); - - test('should enable/disable highlighting if config is changed', () => { - let request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).not.toBe(undefined); - - configMock['doc_table:highlight'] = false; - request = getHighlightRequest(queryStringQuery, getConfig); + const request = getHighlightRequest(queryStringQuery, false); expect(request).toBe(undefined); }); }); diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts index 199a73e692e399..8012ab59c33bae 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts @@ -21,8 +21,8 @@ import { highlightTags } from './highlight_tags'; const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) -export function getHighlightRequest(query: any, getConfig: Function) { - if (!getConfig('doc_table:highlight')) return; +export function getHighlightRequest(query: any, shouldHighlight: boolean) { + if (!shouldHighlight) return; return { pre_tags: [highlightTags.pre], diff --git a/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts new file mode 100644 index 00000000000000..777a12c7e2884a --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; + +export async function getIndexPatternTitle( + client: SavedObjectsClientContract, + indexPatternId: string +): Promise> { + const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; + + if (savedObject.error) { + throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); + } + + return savedObject.attributes.title; +} diff --git a/src/legacy/ui/public/courier/search_strategy/index.d.ts b/src/plugins/data/public/index_patterns/lib/index.ts similarity index 92% rename from src/legacy/ui/public/courier/search_strategy/index.d.ts rename to src/plugins/data/public/index_patterns/lib/index.ts index dc98484655d002..d1c229513aa339 100644 --- a/src/legacy/ui/public/courier/search_strategy/index.d.ts +++ b/src/plugins/data/public/index_patterns/lib/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SearchError, getSearchErrorType } from './search_error'; +export { getIndexPatternTitle } from './get_index_pattern_title'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index ff5c96c2d89edb..ceb57b4a3a564d 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -66,6 +66,9 @@ const createStartContract = (): Start => { search: { search: jest.fn() }, fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, + ui: { + IndexPatternSelect: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 3aa9cd9a0bcb44..d8c45b6786c0cf 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -25,6 +25,7 @@ import { getSuggestionsProvider } from './suggestions_provider'; import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; +import { createIndexPatternSelect } from './ui/index_pattern_select'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); @@ -59,6 +60,9 @@ export class DataPublicPlugin implements Plugin { describe('getTime', () => { @@ -43,8 +43,8 @@ describe('get_time', () => { ], } as any, { from: 'now-60y', to: 'now' } - ) as Filter; - expect(filter.range.date).toEqual({ + ); + expect(filter!.range.date).toEqual({ gte: '1940-02-01T00:00:00.000Z', lte: '2000-02-01T00:00:00.000Z', format: 'strict_date_optional_time', diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index 41ad1a49af0ffe..d3fbc17734f817 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -21,22 +21,13 @@ import dateMath from '@elastic/datemath'; import { TimeRange } from '../../../common'; // TODO: remove this -import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public/index_patterns'; +import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public'; +import { esFilters } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; } -interface RangeFilter { - gte?: string | number; - lte?: string | number; - format: string; -} - -export interface Filter { - range: { [s: string]: RangeFilter }; -} - export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOptions = {}) { return { min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }), @@ -45,10 +36,10 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp } export function getTime( - indexPattern: IndexPattern, + indexPattern: IndexPattern | undefined, timeRange: TimeRange, forceNow?: Date -): Filter | undefined { +) { if (!indexPattern) { // in CI, we sometimes seem to fail here. return; @@ -66,17 +57,13 @@ export function getTime( if (!bounds) { return; } - const filter: Filter = { - range: { [timefield.name]: { format: 'strict_date_optional_time' } }, - }; - - if (bounds.min) { - filter.range[timefield.name].gte = bounds.min.toISOString(); - } - - if (bounds.max) { - filter.range[timefield.name].lte = bounds.max.toISOString(); - } - - return filter; + return esFilters.buildRangeFilter( + timefield, + { + ...(bounds.min && { gte: bounds.min.toISOString() }), + ...(bounds.max && { lte: bounds.max.toISOString() }), + format: 'strict_date_optional_time', + }, + indexPattern + ); } diff --git a/src/plugins/data/public/search/i_search.ts b/src/plugins/data/public/search/i_search.ts index 0e256b960ffa30..a39ef3e3e75719 100644 --- a/src/plugins/data/public/search/i_search.ts +++ b/src/plugins/data/public/search/i_search.ts @@ -49,11 +49,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], - options: ISearchOptions, + options?: ISearchOptions, strategy?: T ) => Observable; export type ISearch = ( request: IRequestTypesMap[T], - options: ISearchOptions + options?: ISearchOptions ) => Observable; diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c412bbb3b104aa..3885a97a98571a 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -34,7 +34,7 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { const search: ISearch = ( request: ISyncSearchRequest, - options: ISearchOptions + options: ISearchOptions = {} ) => { const response: Promise = context.core.http.fetch( `/internal/search/${request.serverStrategy}`, diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index a59e7f3de35888..c0c96372f9f59b 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -24,6 +24,7 @@ import { FieldFormatsSetup, FieldFormatsStart } from './field_formats_provider'; import { ISearchSetup, ISearchStart } from './search'; import { IGetSuggestions } from './suggestions_provider/types'; import { QuerySetup, QueryStart } from './query'; +import { IndexPatternSelectProps } from './ui/index_pattern_select'; export interface DataPublicPluginSetup { autocomplete: AutocompletePublicPluginSetup; @@ -38,6 +39,9 @@ export interface DataPublicPluginStart { search: ISearchStart; fieldFormats: FieldFormatsStart; query: QueryStart; + ui: { + IndexPatternSelect: React.ComponentType; + }; } export * from './autocomplete_provider/types'; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx similarity index 94% rename from src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index 954cbca8f054ba..affbb8acecb201 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,17 +30,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { IndexPattern } from '../../index_patterns'; -import { - mapAndFlattenFilters, - esFilters, - utils, - FilterLabel, -} from '../../../../../../plugins/data/public'; +import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { FilterLabel } from '../filter_bar'; interface Props { filters: esFilters.Filter[]; - indexPatterns: IndexPattern[]; + indexPatterns: IIndexPattern[]; onCancel: () => void; onSubmit: (filters: esFilters.Filter[]) => void; } diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx new file mode 100644 index 00000000000000..71a042adffa391 --- /dev/null +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; +import { IIndexPattern, esFilters } from '../..'; + +type CancelFnType = () => void; +type SubmitFnType = (filters: esFilters.Filter[]) => void; + +export const applyFiltersPopover = ( + filters: esFilters.Filter[], + indexPatterns: IIndexPattern[], + onCancel: CancelFnType, + onSubmit: SubmitFnType +) => { + return ( + + ); +}; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/plugins/data/public/ui/apply_filters/index.ts similarity index 92% rename from src/legacy/core_plugins/data/public/filter/apply_filters/index.ts rename to src/plugins/data/public/ui/apply_filters/index.ts index 6b64230ed6a0c2..93c1245e1ffb0c 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts +++ b/src/plugins/data/public/ui/apply_filters/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters_popover'; +export { applyFiltersPopover } from './apply_filters_popover'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index d0aaf2f6aac1c3..cb7c92b00ea3ad 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export * from './filter_bar'; +export { IndexPatternSelect } from './index_pattern_select'; +export { FilterBar } from './filter_bar'; +export { applyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index.ts b/src/plugins/data/public/ui/index_pattern_select/index.ts similarity index 100% rename from src/legacy/core_plugins/data/public/index_patterns/components/index.ts rename to src/plugins/data/public/ui/index_pattern_select/index.ts diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx rename to src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 77692d7bcaa0de..f868e4b1f7504c 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -21,10 +21,10 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../../core/public'; -import { getIndexPatternTitle } from '../utils'; +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { getIndexPatternTitle } from '../../index_patterns/lib'; -interface IndexPatternSelectProps { +export interface IndexPatternSelectProps { onChange: (opt: any) => void; indexPatternId: string; placeholder: string; diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index 32570a05031f6b..cc13269e1aa21e 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -55,7 +55,7 @@ describe('createApi', () => { }); it('should throw if no provider is found for the given name', () => { - expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( + expect(api.search({}, {}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( `"No strategy found for noneByThisName"` ); }); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 4c13dd9e1137cf..2a874869526d73 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -30,7 +30,7 @@ export function createApi({ caller: APICaller; }) { const api: IRouteHandlerSearchContext = { - search: async (request, strategyName) => { + search: async (request, options, strategyName) => { const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 619a28df839bd8..7b725a47aa13bd 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -66,7 +66,7 @@ describe('ES search strategy', () => { expect(spy).toBeCalled(); }); - it('calls the API caller with the params', () => { + it('calls the API caller with the params with defaults', () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { @@ -80,7 +80,31 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); - expect(mockApiCaller.mock.calls[0][1]).toEqual(params); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + ignoreUnavailable: true, + restTotalHitsAsInt: true, + }); + }); + + it('calls the API caller with overridden defaults', () => { + const params = { index: 'logstash-*', ignoreUnavailable: false }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + esSearch.search({ params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('search'); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + restTotalHitsAsInt: true, + }); }); it('returns total, loaded, and raw response', async () => { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 31f4fc15a09894..c5fc1d9d3a11c2 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search'; +import { ES_SEARCH_STRATEGY } from '../../../common/search'; import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; import { ISearchContext } from '..'; @@ -27,16 +27,17 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { - search: async (request: IEsSearchRequest) => { + search: async (request, options) => { + const params = { + ignoreUnavailable: true, // Don't fail if the index/indices don't exist + restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range + ...request.params, + }; if (request.debug) { // eslint-disable-next-line - console.log(JSON.stringify(request, null, 2)); + console.log(JSON.stringify(params, null, 2)); } - const esSearchResponse = (await caller('search', { - ...request.params, - // TODO: could do something like this here? - // ...getCurrentSearchParams(context), - })) as SearchResponse; + const esSearchResponse = (await caller('search', params, options)) as SearchResponse; // The above query will either complete or timeout and throw an error. // There is no progress indication on this api. diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index fabcb98ceea720..0a357345741534 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -22,6 +22,10 @@ import { TStrategyTypes } from './strategy_types'; import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; import { IEsSearchRequest } from './es_search'; +export interface ISearchOptions { + signal?: AbortSignal; +} + export interface IRequestTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchRequest; [key: string]: IKibanaSearchRequest; @@ -34,9 +38,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], + options?: ISearchOptions, strategy?: T ) => Promise; export type ISearch = ( - request: IRequestTypesMap[T] + request: IRequestTypesMap[T], + options?: ISearchOptions ) => Promise; diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index ebdcf48f608b93..a2394d88f39314 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -60,7 +60,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); @@ -92,7 +92,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.internalError).toBeCalled(); expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6cb6c28c760140..eaa72548e08ee7 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -36,7 +36,7 @@ export function registerSearchRoute(router: IRouter): void { const searchRequest = request.body; const strategy = request.params.strategy; try { - const response = await context.search!.search(searchRequest, strategy); + const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { return res.internalError({ body: err }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 4edb51300dfaf4..3409a72326121f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -77,7 +77,7 @@ export class SearchService implements Plugin { caller, searchStrategies: this.searchStrategies, }); - return searchAPI.search(request, strategyName); + return searchAPI.search(request, {}, strategyName); }, }, }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 33855b07df7a12..ea2bd910b06248 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -61,5 +61,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { EmbeddablePublicPlugin as Plugin }; -export * from './plugin'; +export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index ef1517bb7f1d5b..fd299bc626fb9e 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,14 +17,15 @@ * under the License. */ -import { Plugin } from '.'; +import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked>; -export type Start = jest.Mocked>; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -43,7 +44,7 @@ const createStartContract = (): Start => { }; const createInstance = () => { - const plugin = new Plugin({} as any); + const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: uiActionsPluginMock.createSetupContract(), }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 458c8bfeb8762f..df1f4e5080031e 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -27,7 +27,13 @@ export interface IEmbeddableSetupDependencies { uiActions: IUiActionsSetup; } -export class EmbeddablePublicPlugin implements Plugin { +export interface IEmbeddableSetup { + registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +} + +export type IEmbeddableStart = EmbeddableApi; + +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private api!: EmbeddableApi; @@ -52,6 +58,3 @@ export class EmbeddablePublicPlugin implements Plugin { public stop() {} } - -export type Setup = ReturnType; -export type Start = ReturnType; diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 5b50bddefcdb7d..6d1e15137480a0 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -21,14 +21,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; // eslint-disable-next-line import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests'; import { IUiActionsApi } from 'src/plugins/ui_actions/public'; -import { EmbeddablePublicPlugin } from '../plugin'; +import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: ReturnType; - doStart: (anotherCoreStart?: CoreStart) => ReturnType; + setup: IEmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; uiActions: IUiActionsApi; } diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index a14aaae98fc342..6dc88fd23f29ad 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -20,10 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ExpressionsPublicPlugin(initializerContext); -} - export { ExpressionsPublicPlugin as Plugin }; export * from './plugin'; @@ -31,3 +27,10 @@ export * from './types'; export * from '../common'; export { interpreterProvider, ExpressionInterpret } from './interpreter_provider'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionDataHandler } from './execute'; + +export { RenderResult, ExpressionRenderHandler } from './render'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsPublicPlugin(initializerContext); +} diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 364d5f587bb6f8..3c7008806e779c 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -30,15 +30,17 @@ interface RenderError { export type IExpressionRendererExtraHandlers = Record; +export type RenderResult = RenderId | RenderError; + export class ExpressionRenderHandler { - render$: Observable; + render$: Observable; update$: Observable; events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.BehaviorSubject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; @@ -49,11 +51,11 @@ export class ExpressionRenderHandler { this.eventsSubject = new Rx.Subject(); this.events$ = this.eventsSubject.asObservable().pipe(share()); - this.renderSubject = new Rx.BehaviorSubject(null as RenderId | RenderError | null); + this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null); this.render$ = this.renderSubject.asObservable().pipe( share(), filter(_ => _ !== null) - ) as Observable; + ) as Observable; this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable().pipe(share()); diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json new file mode 100644 index 00000000000000..edebf8cb122391 --- /dev/null +++ b/src/plugins/status_page/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "status_page", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/status_page/public/index.ts b/src/plugins/status_page/public/index.ts new file mode 100644 index 00000000000000..db1f05cac076fb --- /dev/null +++ b/src/plugins/status_page/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new StatusPagePlugin(); diff --git a/src/plugins/status_page/public/plugin.ts b/src/plugins/status_page/public/plugin.ts new file mode 100644 index 00000000000000..d072fd4a67c306 --- /dev/null +++ b/src/plugins/status_page/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/public'; + +export class StatusPagePlugin implements Plugin { + public setup(core: CoreSetup) { + const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( + 'isStatusPageAnonymous' + ) as boolean; + + if (isStatusPageAnonymous) { + core.http.anonymousPaths.register('/status'); + } + } + + public start() {} + + public stop() {} +} + +export type StatusPagePluginSetup = ReturnType; +export type StatusPagePluginStart = ReturnType; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 16947a97a3d146..25723677390bdf 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -34,6 +34,19 @@ module.exports = function (grunt) { return 'Chrome'; } + function pickReporters() { + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + if (process.env.CI && process.env.DISABLE_JUNIT_REPORTER) { + return ['dots']; + } + + if (process.env.CI) { + return ['dots', 'junit']; + } + + return ['progress']; + } + const config = { options: { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -63,14 +76,13 @@ module.exports = function (grunt) { }, }, - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: process.env.CI ? ['dots', 'junit'] : ['progress'], + reporters: pickReporters(), junitReporter: { outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), useBrowserName: false, - nameFormatter: (browser, result) => [...result.suite, result.description].join(' '), - classNameFormatter: (browser, result) => { + nameFormatter: (_, result) => [...result.suite, result.description].join(' '), + classNameFormatter: (_, result) => { const rootSuite = result.suite[0] || result.description; return `Browser Unit Tests.${rootSuite.replace(/\./g, '·')}`; }, diff --git a/tasks/config/run.js b/tasks/config/run.js index ea5a4b01dc8a52..e4071c8b7d0abc 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -254,7 +254,7 @@ module.exports = function (grunt) { cmd: NODE, args: [ 'scripts/functional_tests', - '--config', 'test/interpreter_functional/config.js', + '--config', 'test/interpreter_functional/config.ts', '--bail', '--debug', '--kibana-install-dir', KIBANA_INSTALL_DIR, diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index a8ce4270d42055..ab686f4d5ffec0 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -470,7 +470,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ); } - public async executeAsync(fn: string | ((...args: any[]) => R), ...args: any[]): Promise { + public async executeAsync( + fn: string | ((...args: any[]) => Promise), + ...args: any[] + ): Promise { return await driver.executeAsyncScript( fn, ...cloneDeep(args, arg => { diff --git a/test/interpreter_functional/README.md b/test/interpreter_functional/README.md index 336bfe3405a014..73df0ce4c9f04d 100644 --- a/test/interpreter_functional/README.md +++ b/test/interpreter_functional/README.md @@ -3,7 +3,7 @@ This folder contains interpreter functional tests. Add new test suites into the `test_suites` folder and reference them from the -`config.js` file. These test suites work the same as regular functional test. +`config.ts` file. These test suites work the same as regular functional test. ## Run the test @@ -11,17 +11,17 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/interpreter_functional/config.js +node scripts/functional_tests_server.js --config test/interpreter_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts ``` # Writing tests -Look into test_suites/run_pipeline/basic.js for examples +Look into test_suites/run_pipeline/basic.ts for examples to update baseline screenshots and snapshots run with: ``` -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js --updateBaselines +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts --updateBaselines ``` \ No newline at end of file diff --git a/test/interpreter_functional/config.js b/test/interpreter_functional/config.ts similarity index 76% rename from test/interpreter_functional/config.js rename to test/interpreter_functional/config.ts index e8700262e273a2..0fe7df4d507154 100644 --- a/test/interpreter_functional/config.js +++ b/test/interpreter_functional/config.ts @@ -19,25 +19,26 @@ import path from 'path'; import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -export default async function ({ readConfigFile }) { +export default async function({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); - const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()); + const plugins = allFiles.filter(file => + fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory() + ); return { - testFiles: [ - require.resolve('./test_suites/run_pipeline'), - ], + testFiles: [require.resolve('./test_suites/run_pipeline')], services: functionalConfig.get('services'), pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), esTestCluster: functionalConfig.get('esTestCluster'), apps: functionalConfig.get('apps'), esArchiver: { - directory: path.resolve(__dirname, '../es_archives') + directory: path.resolve(__dirname, '../es_archives'), }, snapshots: { directory: path.resolve(__dirname, 'snapshots'), @@ -49,7 +50,9 @@ export default async function ({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - ...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`), + ...plugins.map( + pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` + ), ], }, }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts similarity index 68% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts index 95d6a555ebcf04..1d5564ec06e4ef 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts @@ -16,24 +16,34 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; +import { + ArrayOrItem, + LegacyPluginApi, + LegacyPluginSpec, + LegacyPluginOptions, +} from 'src/legacy/plugin_discovery/types'; -export default function (kibana) { - return new kibana.Plugin({ +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + const pluginSpec: Partial = { + id: 'kbn_tp_run_pipeline', uiExports: { app: { title: 'Run Pipeline', description: 'This is a sample plugin to test running pipeline expressions', - main: 'plugins/kbn_tp_run_pipeline/app', - } + main: 'plugins/kbn_tp_run_pipeline/legacy', + }, }, - init(server) { + init(server: Legacy.Server) { // The following lines copy over some configuration variables from Kibana // to this plugin. This will be needed when embedding visualizations, so that e.g. // region map is able to get its configuration. server.injectUiAppVars('kbn_tp_run_pipeline', async () => { - return await server.getInjectedUiAppVars('kibana'); + return server.getInjectedUiAppVars('kibana'); }); - } - }); + }, + }; + return new kibana.Plugin(pluginSpec); } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js deleted file mode 100644 index e9ab2a41699156..00000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { registries } from 'plugins/interpreter/registries'; -import { npStart } from 'ui/new_platform'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -// These are all the required uiExports you need to import in case you want to embed visualizations. -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visEditorTypes'; -import 'uiExports/visualize'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/search'; -import 'uiExports/interpreter'; - -import { Main } from './components/main'; - -const app = uiModules.get('apps/kbnRunPipelinePlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(
, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('kbnRunPipelinePlugin', RootController); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js deleted file mode 100644 index 3e19d3a4d78ec6..00000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, -} from '@elastic/eui'; -import { first } from 'rxjs/operators'; - -class Main extends React.Component { - chartDiv = React.createRef(); - - constructor(props) { - super(props); - - this.state = { - expression: '', - }; - - window.runPipeline = async (expression, context = {}, initialContext = {}) => { - this.setState({ expression }); - const adapters = { - requests: new props.RequestAdapter(), - data: new props.DataAdapter(), - }; - return await props.expressions.execute(expression, { - inspectorAdapters: adapters, - context, - searchContext: initialContext, - }).getData(); - }; - - let lastRenderHandler; - window.renderPipelineResponse = async (context = {}) => { - if (lastRenderHandler) { - lastRenderHandler.destroy(); - } - - lastRenderHandler = props.expressions.render(this.chartDiv, context); - const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); - - if (typeof renderResult === 'object' && renderResult.type === 'error') { - return this.setState({ expression: 'Render error!\n\n' + JSON.stringify(renderResult.error) }); - } - }; - } - - - render() { - const pStyle = { - display: 'flex', - width: '100%', - height: '300px' - }; - - return ( - - - - - runPipeline tests are running ... - -
this.chartDiv = ref} style={pStyle}/> -
{this.state.expression}
- - - - ); - } -} - -export { Main }; diff --git a/src/legacy/ui/public/courier/search_source/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts similarity index 94% rename from src/legacy/ui/public/courier/search_source/index.js rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts index dcae7b3d2ff058..c4cc7175d61570 100644 --- a/src/legacy/ui/public/courier/search_source/index.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SearchSource } from './search_source'; +export * from './np_ready'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts new file mode 100644 index 00000000000000..39ce2b3077c961 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { npSetup, npStart } from 'ui/new_platform'; + +import { plugin } from './np_ready'; + +// This is required so some default styles and required scripts/Angular modules are loaded, +// or the timezone setting is correctly applied. +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +// Used for kibana_context function + +import 'uiExports/savedObjectTypes'; +import 'uiExports/interpreter'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx new file mode 100644 index 00000000000000..f47a7c3a256f0a --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { Main } from './components/main'; + +export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { + render(
, element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx new file mode 100644 index 00000000000000..c091765619a194 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; +import { first } from 'rxjs/operators'; +import { + RequestAdapter, + DataAdapter, +} from '../../../../../../../../src/plugins/inspector/public/adapters'; +import { + Adapters, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from '../../types'; +import { getExpressions } from '../../services'; + +declare global { + interface Window { + runPipeline: ( + expressions: string, + context?: Context, + initialContext?: Context + ) => ReturnType; + renderPipelineResponse: (context?: Context) => Promise; + } +} + +interface State { + expression: string; +} + +class Main extends React.Component<{}, State> { + chartRef = React.createRef(); + + constructor(props: {}) { + super(props); + + this.state = { + expression: '', + }; + + window.runPipeline = async ( + expression: string, + context: Context = {}, + initialContext: Context = {} + ) => { + this.setState({ expression }); + const adapters: Adapters = { + requests: new RequestAdapter(), + data: new DataAdapter(), + }; + return getExpressions() + .execute(expression, { + inspectorAdapters: adapters, + context, + // TODO: naming / typing is confusing and doesn't match here + // searchContext is also a way to set initialContext and Context can't be set to SearchContext + searchContext: initialContext as any, + }) + .getData(); + }; + + let lastRenderHandler: ExpressionRenderHandler; + window.renderPipelineResponse = async (context = {}) => { + if (lastRenderHandler) { + lastRenderHandler.destroy(); + } + + lastRenderHandler = getExpressions().render(this.chartRef.current!, context); + const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); + + if (typeof renderResult === 'object' && renderResult.type === 'error') { + this.setState({ + expression: 'Render error!\n\n' + JSON.stringify(renderResult.error), + }); + } + + return renderResult; + }; + } + + render() { + const pStyle = { + display: 'flex', + width: '100%', + height: '300px', + }; + + return ( + + + + runPipeline tests are running ... +
+
{this.state.expression}
+ + + + ); + } +} + +export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts new file mode 100644 index 00000000000000..d7a764b581c01d --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; +import { Plugin, StartDeps } from './plugin'; +export { StartDeps }; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new Plugin(initializerContext); +}; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts similarity index 53% rename from src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts index 7f638d357a9e17..348ba215930b06 100644 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts @@ -17,36 +17,29 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; +import { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { ExpressionsStart } from './types'; +import { setExpressions } from './services'; -interface InspectorStat { - label: string; - value: string; - description: string; +export interface StartDeps { + expressions: ExpressionsStart; } -interface RequestInspectorStats { - indexPattern: InspectorStat; - indexPatternId: InspectorStat; -} +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} -interface ResponseInspectorStats { - queryTime: InspectorStat; - hitsTotal: InspectorStat; - hits: InspectorStat; - requestTime: InspectorStat; -} + public setup({ application }: CoreSetup) { + application.register({ + id: 'kbn_tp_run_pipeline', + title: 'Run Pipeline', + async mount(context, params) { + const { renderApp } = await import('./app/app'); + return renderApp(context, params); + }, + }); + } -interface Response { - took: number; - hits: { - total: number; - hits: any[]; - }; + public start(start: CoreStart, { expressions }: StartDeps) { + setExpressions(expressions); + } } - -export function getRequestInspectorStats(searchSource: SearchSource): RequestInspectorStats; -export function getResponseInspectorStats( - searchSource: SearchSource, - resp: Response -): ResponseInspectorStats; diff --git a/src/legacy/ui/public/courier/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts similarity index 77% rename from src/legacy/ui/public/courier/index.js rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts index 5647af3d0d6457..657d8d5150c3a8 100644 --- a/src/legacy/ui/public/courier/index.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -17,12 +17,7 @@ * under the License. */ -export { SearchSource } from './search_source'; +import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { ExpressionsStart } from './types'; -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - isDefaultTypeIndexPattern, - SearchError, - getSearchErrorType, -} from './search_strategy'; +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts new file mode 100644 index 00000000000000..082bb47d80066a --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from 'src/plugins/expressions/public'; + +import { Adapters } from 'src/plugins/inspector/public'; + +export { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, + Adapters, +}; diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.ts similarity index 69% rename from test/interpreter_functional/test_suites/run_pipeline/basic.js rename to test/interpreter_functional/test_suites/run_pipeline/basic.ts index 893a79956093c8..77853b0bcd6a4b 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -18,13 +18,16 @@ */ import expect from '@kbn/expect'; -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline +// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline // test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('basic visualize loader pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); @@ -39,7 +42,12 @@ export default function ({ getService, updateBaselines }) { }); it('correctly sets timeRange', async () => { - const result = await expectExpression('correctly_sets_timerange', 'kibana', {}, { timeRange: 'test' }).getResponse(); + const result = await expectExpression( + 'correctly_sets_timerange', + 'kibana', + {}, + { timeRange: 'test' } + ).getResponse(); expect(result).to.have.property('timeRange', 'test'); }); }); @@ -60,30 +68,32 @@ export default function ({ getService, updateBaselines }) { // we can also do snapshot comparison of result of our expression // to update the snapshots run the tests with --updateBaselines - it ('runs the expression and compares final output', async () => { + it('runs the expression and compares final output', async () => { await expectExpression('final_output_test', expression).toMatchSnapshot(); }); // its also possible to check snapshot at every step of expression (after execution of each function) - it ('runs the expression and compares output at every step', async () => { + it('runs the expression and compares output at every step', async () => { await expectExpression('step_output_test', expression).steps.toMatchSnapshot(); }); // and we can do screenshot comparison of the rendered output of expression (if expression returns renderable) - it ('runs the expression and compares screenshots', async () => { + it('runs the expression and compares screenshots', async () => { await expectExpression('final_screenshot_test', expression).toMatchScreenshot(); }); // it is also possible to combine different checks - it ('runs the expression and combines different checks', async () => { - await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot(); + it('runs the expression and combines different checks', async () => { + await ( + await expectExpression('combined_test', expression).steps.toMatchSnapshot() + ).toMatchScreenshot(); }); }); // if we want to do multiple different tests using the same data, or reusing a part of expression its // possible to retrieve the intermediate result and reuse it in later expressions describe('reusing partial results', () => { - it ('does some screenshot comparisons', async () => { + it('does some screenshot comparisons', async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, {"id":"2","enabled":true,"type":"terms","schema":"segment","params": @@ -93,17 +103,20 @@ export default function ({ getService, updateBaselines }) { const context = await expectExpression('partial_test', expression).getResponse(); // we reuse that response to render 3 different charts and compare screenshots with baselines - const tagCloudExpr = - `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const metricExpr = - `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const regionMapExpr = - `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; - await (await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + await ( + await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.js b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts similarity index 55% rename from test/interpreter_functional/test_suites/run_pipeline/helpers.js rename to test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 4df86d3418f1fa..e1ec18fae5e3a8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.js +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -18,14 +18,45 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { + ExpressionDataHandler, + RenderResult, + Context, +} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; + +type UnWrapPromise = T extends Promise ? U : T; +export type ExpressionResult = UnWrapPromise>; + +export type ExpectExpression = ( + name: string, + expression: string, + context?: Context, + initialContext?: Context +) => ExpectExpressionHandler; + +export interface ExpectExpressionHandler { + toReturn: (expectedResult: ExpressionResult) => Promise; + getResponse: () => Promise; + runExpression: (step?: string, stepContext?: Context) => Promise; + steps: { + toMatchSnapshot: () => Promise; + }; + toMatchSnapshot: () => Promise; + toMatchScreenshot: () => Promise; +} // helper for testing interpreter expressions -export const expectExpressionProvider = ({ getService, updateBaselines }) => { +export function expectExpressionProvider({ + getService, + updateBaselines, +}: Pick & { updateBaselines: boolean }): ExpectExpression { const browser = getService('browser'); const screenshot = getService('screenshots'); const snapshots = getService('snapshots'); const log = getService('log'); const testSubjects = getService('testSubjects'); + /** * returns a handler object to test a given expression * @name: name of the test @@ -34,20 +65,25 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @initialContext: initialContext provided to the expression * @returns handler object */ - return (name, expression, context = {}, initialContext = {}) => { + return ( + name: string, + expression: string, + context: Context = {}, + initialContext: Context = {} + ): ExpectExpressionHandler => { log.debug(`executing expression ${expression}`); const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast - let responsePromise; + let responsePromise: Promise; - const handler = { + const handler: ExpectExpressionHandler = { /** * checks if provided object matches expression result * @param result: expected expression result * @returns {Promise} */ - toReturn: async result => { + toReturn: async (expectedResult: ExpressionResult) => { const pipelineResponse = await handler.getResponse(); - expect(pipelineResponse).to.eql(result); + expect(pipelineResponse).to.eql(expectedResult); }, /** * returns expression response @@ -63,16 +99,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @param stepContext: context to provide to expression * @returns {Promise<*>} result of running expression */ - runExpression: async (step, stepContext) => { + runExpression: async ( + step: string = expression, + stepContext: Context = context + ): Promise => { log.debug(`running expression ${step || expression}`); - const promise = browser.executeAsync((expression, context, initialContext, done) => { - if (!context) context = {}; - if (!context.type) context.type = 'null'; - window.runPipeline(expression, context, initialContext).then(result => { - done(result); - }); - }, step || expression, stepContext || context, initialContext); - return await promise; + return browser.executeAsync( + ( + _expression: string, + _currentContext: Context & { type: string }, + _initialContext: Context, + done: (expressionResult: ExpressionResult) => void + ) => { + if (!_currentContext) _currentContext = { type: 'null' }; + if (!_currentContext.type) _currentContext.type = 'null'; + return window + .runPipeline(_expression, _currentContext, _initialContext) + .then(expressionResult => { + done(expressionResult); + return expressionResult; + }); + }, + step, + stepContext, + initialContext + ); }, steps: { /** @@ -80,17 +131,19 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @returns {Promise} */ toMatchSnapshot: async () => { - let lastResponse; + let lastResponse: ExpressionResult; for (let i = 0; i < steps.length; i++) { const step = steps[i]; - lastResponse = await handler.runExpression(step, lastResponse); - const diff = await snapshots.compareAgainstBaseline(name + i, toSerializable(lastResponse), updateBaselines); + lastResponse = await handler.runExpression(step, lastResponse!); + const diff = await snapshots.compareAgainstBaseline( + name + i, + toSerializable(lastResponse!), + updateBaselines + ); expect(diff).to.be.lessThan(0.05); } if (!responsePromise) { - responsePromise = new Promise(resolve => { - resolve(lastResponse); - }); + responsePromise = Promise.resolve(lastResponse!); } return handler; }, @@ -101,7 +154,11 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { */ toMatchSnapshot: async () => { const pipelineResponse = await handler.getResponse(); - await snapshots.compareAgainstBaseline(name, toSerializable(pipelineResponse), updateBaselines); + await snapshots.compareAgainstBaseline( + name, + toSerializable(pipelineResponse), + updateBaselines + ); return handler; }, /** @@ -111,24 +168,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { toMatchScreenshot: async () => { const pipelineResponse = await handler.getResponse(); log.debug('starting to render'); - const result = await browser.executeAsync((context, done) => { - window.renderPipelineResponse(context).then(result => { - done(result); - }); - }, pipelineResponse); + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: RenderResult) => void) => + window.renderPipelineResponse(_context).then(renderResult => { + done(renderResult); + return renderResult; + }), + pipelineResponse + ); log.debug('response of rendering: ', result); const chartEl = await testSubjects.find('pluginChart'); - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines, chartEl); + const percentDifference = await screenshot.compareAgainstBaseline( + name, + updateBaselines, + chartEl + ); expect(percentDifference).to.be.lessThan(0.1); return handler; - } + }, }; return handler; }; - function toSerializable(response) { + function toSerializable(response: ExpressionResult) { if (response.error) { // in case of error, pass through only message to the snapshot // as error could be expected and stack trace shouldn't be part of the snapshot @@ -136,4 +200,4 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { } return response; } -}; +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.ts similarity index 82% rename from test/interpreter_functional/test_suites/run_pipeline/index.js rename to test/interpreter_functional/test_suites/run_pipeline/index.ts index 3c1ce2314f55f0..031a0e3576ccc0 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -17,7 +17,9 @@ * under the License. */ -export default function ({ getService, getPageObjects, loadTestFile }) { +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -25,13 +27,16 @@ export default function ({ getService, getPageObjects, loadTestFile }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header']); - describe('runPipeline', function () { + describe('runPipeline', function() { this.tags(['skipFirefox']); before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' }); + await kibanaServer.uiSettings.replace({ + 'dateFormat:tz': 'Australia/North', + defaultIndex: 'logstash-*', + }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); await appsMenu.clickLink('Run Pipeline'); diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.ts similarity index 64% rename from test/interpreter_functional/test_suites/run_pipeline/metric.js rename to test/interpreter_functional/test_suites/run_pipeline/metric.ts index 78d571b3583bef..c238bedfa28ce1 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.js +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('metricVis pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders metric', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -44,27 +47,46 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with single metric data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_single_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with multiple metric data', async () => { const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; - await (await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_multi_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; - await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with percentage option', async () => { - const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; - await (await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + await ( + await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts similarity index 61% rename from test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js rename to test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts index 7c0e2d7190703e..2451df4db6310e 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js +++ b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('tag cloud pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders tagcloud', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -41,27 +44,39 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with just metric data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}'; - await (await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with font size options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; - await (await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; + await ( + await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with scale and orientation options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; - await (await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; + await ( + await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/plugin_functional/plugins/demo_search/server/constants.ts b/test/plugin_functional/plugins/demo_search/server/constants.ts deleted file mode 100644 index 11c258a21d5a86..00000000000000 --- a/test/plugin_functional/plugins/demo_search/server/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY'; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index f03b3c4a1e0a5e..6b82a67b9fcdae 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -27,7 +27,7 @@ import { Setup as InspectorSetupContract, } from '../../../../../../../src/plugins/inspector/public'; -import { Plugin as EmbeddablePlugin, CONTEXT_MENU_TRIGGER } from './embeddable_api'; +import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; @@ -38,9 +38,13 @@ import { ContactCardEmbeddableFactory, } from './embeddable_api'; import { App } from './app'; +import { + IEmbeddableStart, + IEmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { SavedObjectFinder: React.ComponentType; @@ -49,7 +53,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; inspector: InspectorStartContract; __LEGACY: { diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 1a7a1c973102ca..27f73c0b6e20d2 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -6,7 +6,7 @@ export TEST_BROWSER_HEADLESS=1 echo " -> Running mocha tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Mocha" yarn test +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser echo "" echo "" diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 74e24692f59f65..d3f93c29e3df89 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -8,7 +8,7 @@ require('../src/setup_node_env'); const { buildTask } = require('./tasks/build'); const { devTask } = require('./tasks/dev'); -const { testTask, testBrowserTask, testBrowserDevTask, testServerTask } = require('./tasks/test'); +const { testTask, testBrowserTask, testBrowserDevTask } = require('./tasks/test'); const { prepareTask } = require('./tasks/prepare'); // export the tasks that are runnable from the CLI @@ -17,7 +17,6 @@ module.exports = { dev: devTask, prepare: prepareTask, test: testTask, - testserver: testServerTask, testbrowser: testBrowserTask, 'testbrowser-dev': testBrowserDevTask, }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx index 035015c82a0ac2..7a23c9f7de842f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); describe('Home component', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 8ddf48e79f911d..41fb12be284ad4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); const coreMock = { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts deleted file mode 100644 index bb9f581129c5e5..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { useEffect } from 'react'; -import { capabilities } from 'ui/capabilities'; -import { useKibanaCore } from '../../../../../observability/public'; - -export const useUpdateBadgeEffect = () => { - const { chrome } = useKibanaCore(); - - useEffect(() => { - const uiCapabilities = capabilities.get(); - chrome.setBadge( - !uiCapabilities.apm.save - ? { - text: i18n.translate('xpack.apm.header.badge.readOnly.text', { - defaultMessage: 'Read only' - }), - tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save' - }), - iconType: 'glasses' - } - : undefined - ); - }, [chrome]); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 881e5975fc81fd..05094c59712a92 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -18,8 +18,6 @@ import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; -jest.mock('ui/kfetch'); - const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); const MockUrlParamsProvider: React.FC<{ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 66946e5b447f90..52be4d4fba7748 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,8 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; @@ -18,25 +16,24 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { + AutocompleteSuggestion, + AutocompleteProvider, + IIndexPattern +} from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; +import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; `; -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - interface State { suggestions: AutocompleteSuggestion[]; isLoadingSuggestions: boolean; } -function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { const ast = fromKueryExpression(kuery); return toElasticsearchQuery(ast, indexPattern); } @@ -44,10 +41,10 @@ function convertKueryToEsQuery( function getSuggestions( query: string, selectionStart: number, - indexPattern: StaticIndexPattern, - boolFilter: unknown + indexPattern: IIndexPattern, + boolFilter: unknown, + autocompleteProvider?: AutocompleteProvider ) { - const autocompleteProvider = getAutocompleteProvider('kuery'); if (!autocompleteProvider) { return []; } @@ -74,6 +71,8 @@ export function KueryBar() { }); const { urlParams } = useUrlParams(); const location = useLocation(); + const { data } = usePlugins(); + const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -108,7 +107,8 @@ export function KueryBar() { inputValue, selectionStart, indexPattern, - boolFilter + boolFilter, + autocompleteProvider ) ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index 8fd3cb0893dea2..db14e1c520020e 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -6,43 +6,18 @@ import { npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; +import { PluginInitializerContext } from 'kibana/public'; import 'ui/autoload/all'; import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import url from 'url'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; import { plugin } from './new-platform'; import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; -const { core } = npStart; - -// render APM feedback link in global help menu -core.chrome.setHelpExtension({ - appName: i18n.translate('xpack.apm.feedbackMenu.appName', { - defaultMessage: 'APM' - }), - links: [ - { - linkType: 'discuss', - href: 'https://discuss.elastic.co/c/apm' - }, - { - linkType: 'custom', - href: url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), - hash: '/management/elasticsearch/upgrade_assistant' - }), - content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { - defaultMessage: 'Upgrade assistant' - }) - } - ] -}); +const { core, plugins } = npStart; +// This will be moved to core.application.register when the new platform +// migration is complete. // @ts-ignore chrome.setRootTemplate(template); @@ -57,5 +32,5 @@ const checkForRoot = () => { }); }; checkForRoot().then(() => { - plugin().start(core); + plugin({} as PluginInitializerContext).start(core, plugins); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx index cb4cc2a845a4c6..9dce4bcdd828cd 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { PluginInitializer } from '../../../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; -export function plugin() { - return new Plugin(); -} +export const plugin: PluginInitializer< + ApmPluginSetup, + ApmPluginStart +> = _core => new ApmPlugin(); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index ac4aca4c795b7e..b5986610d3048f 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext, createContext } from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { LegacyCoreStart } from 'src/core/public'; +import { + CoreStart, + LegacyCoreStart, + Plugin, + CoreSetup +} from '../../../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { KibanaCoreContextProvider } from '../../../observability/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; @@ -19,9 +25,10 @@ import { LicenseProvider } from '../context/LicenseContext'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; +import { setHelpExtension } from './setHelpExtension'; +import { setReadonlyBadge } from './updateBadge'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -31,41 +38,70 @@ const MainContainer = styled.main` `; const App = () => { - useUpdateBadgeEffect(); - return ( - - - - - - - - - {routes.map((route, i) => ( - - ))} - - - - - - + + + + + {routes.map((route, i) => ( + + ))} + + ); }; -export class Plugin { - public start(core: LegacyCoreStart) { - const { i18n } = core; +export type ApmPluginSetup = void; +export type ApmPluginStart = void; +export type ApmPluginSetupDeps = {}; // eslint-disable-line @typescript-eslint/consistent-type-definitions + +export interface ApmPluginStartDeps { + data: DataPublicPluginStart; +} + +const PluginsContext = createContext({} as ApmPluginStartDeps); + +export function usePlugins() { + return useContext(PluginsContext); +} + +export class ApmPlugin + implements + Plugin< + ApmPluginSetup, + ApmPluginStart, + ApmPluginSetupDeps, + ApmPluginStartDeps + > { + // Take the DOM element as the constructor, so we can mount the app. + public setup(_core: CoreSetup, _plugins: ApmPluginSetupDeps) {} + + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + const i18nCore = core.i18n; + + // render APM feedback link in global help menu + setHelpExtension(core); + setReadonlyBadge(core); + ReactDOM.render( - - - - - - - - + + + + + + + + + + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); @@ -76,4 +112,6 @@ export class Plugin { console.log('Error fetching static index pattern', e); }); } + + public stop() {} } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts new file mode 100644 index 00000000000000..1a3394651b2ff2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import url from 'url'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setHelpExtension({ chrome, http }: CoreStart) { + chrome.setHelpExtension({ + appName: i18n.translate('xpack.apm.feedbackMenu.appName', { + defaultMessage: 'APM' + }), + links: [ + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/apm' + }, + { + linkType: 'custom', + href: url.format({ + pathname: http.basePath.prepend('/app/kibana'), + hash: '/management/elasticsearch/upgrade_assistant' + }), + content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { + defaultMessage: 'Upgrade assistant' + }) + } + ] + }); +} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts new file mode 100644 index 00000000000000..b3e29bb891c23e --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setReadonlyBadge({ application, chrome }: CoreStart) { + const canSave = application.capabilities.apm.save; + const { setBadge } = chrome; + + setBadge( + !canSave + ? { + text: i18n.translate('xpack.apm.header.badge.readOnly.text', { + defaultMessage: 'Read only' + }), + tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save' + }), + iconType: 'glasses' + } + : undefined + ); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 8f19f4baed7ee9..a09cdbf91ec6ed 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -6,7 +6,6 @@ import moment from 'moment'; import { KibanaRequest } from 'src/core/server'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { IIndexPattern } from 'src/plugins/data/common'; import { APMConfig } from '../../../../../../plugins/apm/server'; import { @@ -22,7 +21,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; function decodeUiFilters( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFiltersEncoded?: string ) { if (!uiFiltersEncoded || !indexPattern) { diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index f113e645ed95fb..9eb99b7c21e751 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; import { APICaller } from 'src/core/server'; import LRU from 'lru-cache'; import { @@ -51,7 +50,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices }); - const indexPattern: StaticIndexPattern = { + const indexPattern: IIndexPattern = { fields, title: indexPatternTitle }; diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js index a87999873e40f0..616aefaf73f62b 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { GrokdebuggerRequest } from '../grokdebugger_request'; -describe('grokdebugger_request', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('grokdebugger_request', () => { describe('GrokdebuggerRequest', () => { const downstreamRequest = { diff --git a/x-pack/legacy/plugins/infra/types/eui.d.ts b/x-pack/legacy/plugins/infra/types/eui.d.ts index 2907830ff882f1..7cf0a91e88c1f6 100644 --- a/x-pack/legacy/plugins/infra/types/eui.d.ts +++ b/x-pack/legacy/plugins/infra/types/eui.d.ts @@ -68,6 +68,7 @@ declare module '@elastic/eui' { rowProps?: any; cellProps?: any; responsive?: boolean; + itemIdToExpandedRowMap?: any; }; export const EuiInMemoryTable: React.FC; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 354a5186db4c11..f7399255b20018 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -15,8 +15,8 @@ import { ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; import { - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../../../src/plugins/embeddable/public'; import { setup as dataSetup, @@ -36,13 +36,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: typeof dataSetup; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: typeof dataStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; expressions: ExpressionsStart; chrome: Chrome; } diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 691c679e5290bf..3b2f887e13c875 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -68,7 +68,7 @@ export const ZOOM_PRECISION = 2; export const ES_SIZE_LIMIT = 10000; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; -export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__'; +export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 90d4ddbeb00928..2f34ffa660d6ef 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -1,4 +1,5 @@
+
+

{{screenTitle}}

diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 41c618d68a68e4..b9354dd0a0dddd 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -66,6 +66,7 @@ const app = uiModules.get(MAP_APP_PATH, []); app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { const { filterManager } = npStart.plugins.data.query; const savedMap = $route.current.locals.map; + $scope.screenTitle = savedMap.title; let unsubscribe; let initialLayerListConfig; const $state = new AppState(); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 44629d16e6fb34..01c323d73f19e2 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,7 +16,6 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; @@ -25,6 +24,9 @@ import { indexPatternService, } from '../../../../kibana_services'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index ef2819f1f372c6..4b04251edd94a0 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -11,7 +11,6 @@ import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS, - FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, @@ -81,12 +80,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { features.push({ type: 'Feature', geometry: tmpGeometriesAccumulator[j], - properties: { - ...properties, - // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions - // Need to prefix with _index to guarantee uniqueness - [FEATURE_ID_PROPERTY_NAME]: `${properties._index}:${properties._id}:${j}` - } + // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions + // Need to prefix with _index to guarantee uniqueness + id: `${properties._index}:${properties._id}:${j}`, + properties, }); } } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index 0b84b4c32f4ac2..45aa2af15eb9d5 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -74,8 +74,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', }, @@ -139,8 +139,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', myField: 8 @@ -152,8 +152,8 @@ describe('hitsToGeoJson', () => { coordinates: [110, 30], type: 'Point', }, + id: 'index1:doc1:1', properties: { - __kbn__feature_id__: 'index1:doc1:1', _id: 'doc1', _index: 'index1', myField: 8 diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 12fab24d1f8d65..71690145427106 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../../../../../src/legacy/ui/public/courier'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { esFilters } from '../../../../../src/plugins/data/public'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; -export { SearchSource } from 'ui/courier'; +export { SearchSource } from '../../../../../src/legacy/ui/public/courier'; export const indexPatternService = data.indexPatterns.indexPatterns; export async function fetchSearchSourceAndRecordWithInspector({ diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 72a89046ed2f5a..1c2f33df66bf89 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -6,8 +6,6 @@ import _ from 'lodash'; import React from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; import { MAX_ZOOM, @@ -19,9 +17,6 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; -const SOURCE_UPDATE_REQUIRED = true; -const NO_SOURCE_UPDATE_REQUIRED = false; - export class AbstractLayer { constructor({ layerDescriptor, source }) { @@ -316,42 +311,7 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } - updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - const { buffer: previousBuffer } = prevMeta; - const { buffer: newBuffer } = nextMeta; - - if (!previousBuffer) { - return SOURCE_UPDATE_REQUIRED; - } - - if (_.isEqual(previousBuffer, newBuffer)) { - return NO_SOURCE_UPDATE_REQUIRED; - } - - const previousBufferGeometry = turf.bboxPolygon([ - previousBuffer.minLon, - previousBuffer.minLat, - previousBuffer.maxLon, - previousBuffer.maxLat - ]); - const newBufferGeometry = turf.bboxPolygon([ - newBuffer.minLon, - newBuffer.minLat, - newBuffer.maxLon, - newBuffer.maxLat - ]); - const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); - - const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); - return doesPreviousBufferContainNewBuffer && !isTrimmed - ? NO_SOURCE_UPDATE_REQUIRED - : SOURCE_UPDATE_REQUIRED; - } getLayerTypeIconName() { throw new Error('should implement Layer#getLayerTypeIconName'); @@ -407,4 +367,3 @@ export class AbstractLayer { } } - diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.test.js b/x-pack/legacy/plugins/maps/public/layers/layer.test.js deleted file mode 100644 index 98be0855cd4b7b..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/layer.test.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AbstractLayer } from './layer'; - -describe('layer', () => { - const layer = new AbstractLayer({ layerDescriptor: {} }); - - describe('updateDueToExtent', () => { - - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return false; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when buffers are the same', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when meta has no old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when the new buffer is not contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 11a02d58a91980..920253d15eaee1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -10,7 +10,6 @@ import { ES_GEO_FIELD_TYPE, GEOJSON_FILE, ES_SIZE_LIMIT, - FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; import { ESSearchSource } from '../es_search_source'; @@ -137,21 +136,8 @@ export class GeojsonFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const copiedPropsFeatures = this._descriptor.__featureCollection.features.map((feature, index) => { - const properties = feature.properties ? { ...feature.properties } : {}; - properties[FEATURE_ID_PROPERTY_NAME] = index; - return { - type: 'Feature', - geometry: feature.geometry, - properties, - }; - }); - return { - data: { - type: 'FeatureCollection', - features: copiedPropsFeatures - }, + data: this._descriptor.__featureCollection, meta: {} }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index b2e04f56e57181..fcd52683b70fff 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,7 +7,7 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { EMS_FILE, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; @@ -94,7 +94,7 @@ export class EMSFileSource extends AbstractVectorSource { return field.type === 'id'; }); featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = emsIdField + feature.id = emsIdField ? feature.properties[emsIdField.id] : index; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js index c83f12ce992ff0..d26bfd8bbeacb3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js @@ -6,7 +6,7 @@ import { RENDER_AS } from './render_as'; import { getTileBoundingBox } from './geo_tile_utils'; -import { EMPTY_FEATURE_COLLECTION, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMPTY_FEATURE_COLLECTION } from '../../../../common/constants'; export function convertToGeoJson({ table, renderAs }) { @@ -34,9 +34,7 @@ export function convertToGeoJson({ table, renderAs }) { return; } - const properties = { - [FEATURE_ID_PROPERTY_NAME]: gridKey - }; + const properties = {}; metricColumns.forEach(metricColumn => { properties[metricColumn.aggConfig.id] = row[metricColumn.id]; }); @@ -49,6 +47,7 @@ export function convertToGeoJson({ table, renderAs }) { geocentroidColumn, renderAs, }), + id: gridKey, properties }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index 395b6ac5cc431d..3d02b075b3b812 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from './render_as'; import { indexPatternService } from '../../../kibana_services'; @@ -22,6 +21,9 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField({ type }) { return [ES_GEO_FIELD_TYPE.GEO_POINT].includes(type); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js index c334776e6c4e87..ae9435dc42c69c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js @@ -6,8 +6,6 @@ import _ from 'lodash'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; - const LAT_INDEX = 0; const LON_INDEX = 1; @@ -47,10 +45,10 @@ export function convertToLines(esResponse) { type: 'LineString', coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest] }, + id: `${dest.join()},${key}`, properties: { - [FEATURE_ID_PROPERTY_NAME]: `${dest.join()},${key}`, ...rest - } + }, }); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js index 9f9789374274aa..897ded43be28b5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; @@ -20,6 +19,8 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; const GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT]; function filterGeoField({ type }) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js index 61300ed209c1fc..a6ba31366d5046 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js @@ -9,7 +9,6 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSpacer, EuiSwitch, EuiCallOut } from '@elastic/eui'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout'; @@ -19,6 +18,9 @@ import { kfetch } from 'ui/kfetch'; import { ES_GEO_FIELD_TYPE, GIS_API_PATH, ES_SIZE_LIMIT } from '../../../../common/constants'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField(field) { return [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes(field.type); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index ffccb18a691926..e29887edcf7d9f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,7 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { FIELD_ORIGIN } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; export class KibanaRegionmapSource extends AbstractVectorSource { @@ -91,9 +91,6 @@ export class KibanaRegionmapSource extends AbstractVectorSource { featureCollectionPath: vectorFileMeta.meta.feature_collection_path, fetchUrl: vectorFileMeta.url }); - featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = index; - }); return { data: featureCollection }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js new file mode 100644 index 00000000000000..2c0d08f86cfc0e --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +let idCounter = 0; + +function generateNumericalId() { + const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; + idCounter = newId + 1; + return newId; +} + +export function assignFeatureIds(featureCollection) { + + // wrt https://github.com/elastic/kibana/issues/39317 + // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. + // This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. + // This is a work-around to avoid hitting such a worst-case + // This was tested as a suitable work-around for mapbox-gl 0.54 + // The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 + + // This only shuffles the id-assignment, _not_ the features in the collection + // The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. + const ids = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const id = generateNumericalId(); + ids.push(id); + } + + const randomizedIds = _.shuffle(ids); + const features = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const numericId = randomizedIds[i]; + const feature = featureCollection.features[i]; + features.push({ + type: 'Feature', + geometry: feature.geometry, // do not copy geometry, this object can be massive + properties: { + // preserve feature id provided by source so features can be referenced across fetches + [FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, + // create new object for properties so original is not polluted with kibana internal props + ...feature.properties, + }, + id: numericId, // Mapbox feature state id, must be integer + }); + } + + return { + type: 'FeatureCollection', + features + }; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js new file mode 100644 index 00000000000000..0678070f568a21 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { assignFeatureIds } from './assign_feature_ids'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +const featureId = 'myFeature1'; + +test('should provide unique id when feature.id is not provided', () => { + const featureCollection = { + features: [ + { + properties: {} + }, + { + properties: {} + }, + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + const feature2 = updatedFeatureCollection.features[1]; + expect(typeof feature1.id).toBe('number'); + expect(typeof feature2.id).toBe('number'); + expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature2.id); +}); + +test('should preserve feature id when provided', () => { + const featureCollection = { + features: [ + { + id: featureId, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); +}); + +test('should preserve feature id for falsy value', () => { + const featureCollection = { + features: [ + { + id: 0, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0); +}); + +test('should not modify original feature properties', () => { + const featureProperties = {}; + const featureCollection = { + features: [ + { + id: featureId, + properties: featureProperties + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME); +}); + diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js new file mode 100644 index 00000000000000..610c704b34ec67 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import { isRefreshOnlyQuery } from './is_refresh_only_query'; + +const SOURCE_UPDATE_REQUIRED = true; +const NO_SOURCE_UPDATE_REQUIRED = false; + +export function updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { + const extentAware = source.isFilterByMapBounds(); + if (!extentAware) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const { buffer: previousBuffer } = prevMeta; + const { buffer: newBuffer } = nextMeta; + + if (!previousBuffer) { + return SOURCE_UPDATE_REQUIRED; + } + + if (_.isEqual(previousBuffer, newBuffer)) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const previousBufferGeometry = turf.bboxPolygon([ + previousBuffer.minLon, + previousBuffer.minLat, + previousBuffer.maxLon, + previousBuffer.maxLat + ]); + const newBufferGeometry = turf.bboxPolygon([ + newBuffer.minLon, + newBuffer.minLat, + newBuffer.maxLon, + newBuffer.maxLat + ]); + const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); + + const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); + return doesPreviousBufferContainNewBuffer && !isTrimmed + ? NO_SOURCE_UPDATE_REQUIRED + : SOURCE_UPDATE_REQUIRED; +} + +export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) { + + const timeAware = await source.isTimeAware(); + const refreshTimerAware = await source.isRefreshTimerAware(); + const extentAware = source.isFilterByMapBounds(); + const isFieldAware = source.isFieldAware(); + const isQueryAware = source.isQueryAware(); + const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); + + if ( + !timeAware && + !refreshTimerAware && + !extentAware && + !isFieldAware && + !isQueryAware && + !isGeoGridPrecisionAware + ) { + return (prevDataRequest && prevDataRequest.hasDataOrRequestInProgress()); + } + + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + let updateDueToTime = false; + if (timeAware) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } + + let updateDueToRefreshTimer = false; + if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { + updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); + } + + let updateDueToFields = false; + if (isFieldAware) { + updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); + } + + let updateDueToQuery = false; + let updateDueToFilters = false; + let updateDueToSourceQuery = false; + let updateDueToApplyGlobalQuery = false; + if (isQueryAware) { + updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; + updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { + updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); + updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); + } else { + // Global filters and query are not applied to layer search request so no re-fetch required. + // Exception is "Refresh" query. + updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); + } + } + + let updateDueToPrecisionChange = false; + if (isGeoGridPrecisionAware) { + updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); + } + + const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + + const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); + + return !updateDueToTime + && !updateDueToRefreshTimer + && !updateDueToExtentChange + && !updateDueToFields + && !updateDueToQuery + && !updateDueToFilters + && !updateDueToSourceQuery + && !updateDueToApplyGlobalQuery + && !updateDueToPrecisionChange + && !updateDueToSourceMetaChange; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js new file mode 100644 index 00000000000000..77359a6def48f5 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; +import { DataRequest } from './data_request'; + +describe('updateDueToExtent', () => { + + it('should be false when the source is not extent aware', async () => { + const sourceMock = { + isFilterByMapBounds: () => { return false; } + }; + expect(updateDueToExtent(sourceMock)).toBe(false); + }); + + describe('source is extent aware', () => { + const sourceMock = { + isFilterByMapBounds: () => { return true; } + }; + + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })) + .toBe(false); + }); + + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); + + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent( + sourceMock, + { buffer: oldBuffer, areResultsTrimmed: true }, + { buffer: newBuffer } + )).toBe(true); + }); + + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent(sourceMock)).toBe(true); + }); + + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); + }); + }); +}); + +describe('canSkipSourceUpdate', () => { + const SOURCE_DATA_REQUEST_ID = 'foo'; + + describe('isQueryAware', () => { + + const queryAwareSourceMock = { + isTimeAware: () => { return false; }, + isRefreshTimerAware: () => { return false; }, + isFilterByMapBounds: () => { return false; }, + isFieldAware: () => { return false; }, + isQueryAware: () => { return true; }, + isGeoGridPrecisionAware: () => { return false; }, + }; + const prevFilters = []; + const prevQuery = { + language: 'kuery', + query: 'machine.os.keyword : "win 7"', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' + }; + + describe('applyGlobalQuery is false', () => { + + const prevApplyGlobalQuery = false; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + + describe('applyGlobalQuery is true', () => { + + const prevApplyGlobalQuery = true; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can not skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js new file mode 100644 index 00000000000000..393c290d696682 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +const VISIBILITY_FILTER_CLAUSE = ['all', + [ + '==', + ['get', FEATURE_VISIBLE_PROPERTY_NAME], + true + ] +]; + +const CLOSED_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] +]; + +const VISIBLE_CLOSED_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + CLOSED_SHAPE_MB_FILTER, +]; + +const ALL_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] +]; + +const VISIBLE_ALL_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + ALL_SHAPE_MB_FILTER, +]; + +const POINT_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] +]; + +const VISIBLE_POINT_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + POINT_MB_FILTER, +]; + +export function getFillFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER; +} + +export function getLineFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER; +} + +export function getPointFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 362c7bfd725407..57126bb7681b85 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -10,7 +10,6 @@ import { AbstractLayer } from './layer'; import { VectorStyle } from './styles/vector/vector_style'; import { InnerJoin } from './joins/inner_join'; import { - GEO_JSON_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, @@ -19,55 +18,16 @@ import { } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; -import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; - -const VISIBILITY_FILTER_CLAUSE = ['all', - [ - '==', - ['get', FEATURE_VISIBLE_PROPERTY_NAME], - true - ] -]; - -const FILL_LAYER_MB_FILTER = [ - ...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] - ] -]; - -const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] - ] -]; - -const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] - ] -]; - - -let idCounter = 0; - -function generateNumericalId() { - const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; - idCounter = newId + 1; - return newId; -} - +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { assignFeatureIds } from './util/assign_feature_ids'; +import { + getFillFilterExpression, + getLineFilterExpression, + getPointFilterExpression, +} from './util/mb_filter_expressions'; export class VectorLayer extends AbstractLayer { @@ -116,6 +76,10 @@ export class VectorLayer extends AbstractLayer { }); } + _hasJoins() { + return this.getValidJoins().length > 0; + } + isDataLoaded() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { @@ -265,109 +229,31 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - async _canSkipSourceUpdate(source, sourceDataId, nextMeta) { - - const timeAware = await source.isTimeAware(); - const refreshTimerAware = await source.isRefreshTimerAware(); - const extentAware = source.isFilterByMapBounds(); - const isFieldAware = source.isFieldAware(); - const isQueryAware = source.isQueryAware(); - const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); - - if ( - !timeAware && - !refreshTimerAware && - !extentAware && - !isFieldAware && - !isQueryAware && - !isGeoGridPrecisionAware - ) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()); - } - - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - if (!sourceDataRequest) { - return false; - } - const prevMeta = sourceDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - let updateDueToTime = false; - if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - } - - let updateDueToRefreshTimer = false; - if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { - updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); - } - - let updateDueToFields = false; - if (isFieldAware) { - updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); - } - - let updateDueToQuery = false; - let updateDueToFilters = false; - let updateDueToSourceQuery = false; - let updateDueToApplyGlobalQuery = false; - if (isQueryAware) { - updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; - updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); - if (nextMeta.applyGlobalQuery) { - updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); - updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); - } else { - // Global filters and query are not applied to layer search request so no re-fetch required. - // Exception is "Refresh" query. - updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); - } - } - - let updateDueToPrecisionChange = false; - if (isGeoGridPrecisionAware) { - updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); - } - const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta); - - const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); - - return !updateDueToTime - && !updateDueToRefreshTimer - && !updateDueToExtentChange - && !updateDueToFields - && !updateDueToQuery - && !updateDueToFilters - && !updateDueToSourceQuery - && !updateDueToApplyGlobalQuery - && !updateDueToPrecisionChange - && !updateDueToSourceMetaChange; - } async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - const searchFilters = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters); - if (canSkip) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - const propertiesMap = sourceDataRequest ? sourceDataRequest.getData() : null; + const prevDataRequest = this._findDataRequestForSource(sourceDataId); + + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { dataHasChanged: false, join: join, - propertiesMap: propertiesMap + propertiesMap: prevDataRequest.getData() }; } @@ -466,27 +352,32 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); - const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); - if (canSkip) { - const sourceDataRequest = this.getSourceDataRequest(); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkipFetch = await canSkipSourceUpdate({ + source: this._source, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { refreshed: false, - featureCollection: sourceDataRequest.getData() + featureCollection: prevDataRequest.getData() }; } try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); const layerName = await this.getDisplayName(); - const { data: featureCollection, meta } = + const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback.bind(null, requestToken) ); - this._assignIdsToFeatures(featureCollection); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, layerFeatureCollection, meta); return { refreshed: true, - featureCollection: featureCollection + featureCollection: layerFeatureCollection }; } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -498,44 +389,21 @@ export class VectorLayer extends AbstractLayer { } } - _assignIdsToFeatures(featureCollection) { - - //wrt https://github.com/elastic/kibana/issues/39317 - //In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. - //This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. - //This is a work-around to avoid hitting such a worst-case - //This was tested as a suitable work-around for mapbox-gl 0.54 - //The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 - - //This only shuffles the id-assignment, _not_ the features in the collection - //The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. - const ids = []; - for (let i = 0; i < featureCollection.features.length; i++) { - const id = generateNumericalId(); - ids.push(id); - } - - const randomizedIds = _.shuffle(ids); - for (let i = 0; i < featureCollection.features.length; i++) { - const id = randomizedIds[i]; - const feature = featureCollection.features[i]; - feature.id = id; // Mapbox feature state id, must be integer - } - } - async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } const sourceResult = await this._syncSource(syncContext); - if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) { + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this._hasJoins()) { return; } const joinStates = await this._syncJoins(syncContext); await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); - } _getSourceFeatureCollection() { @@ -605,7 +473,11 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(pointLayerId)) { + mbMap.setFilter(pointLayerId, filterExpr); } this._style.setMBPaintPropertiesForPoints({ @@ -626,7 +498,11 @@ export class VectorLayer extends AbstractLayer { type: 'symbol', source: sourceId, }); - mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(symbolLayerId)) { + mbMap.setFilter(symbolLayerId, filterExpr); } this._style.setMBSymbolPropertiesForPoints({ @@ -640,6 +516,7 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { mbMap.addLayer({ id: fillLayerId, @@ -647,7 +524,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER); } if (!mbMap.getLayer(lineLayerId)) { mbMap.addLayer({ @@ -656,7 +532,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER); } this._style.setMBPaintProperties({ alpha: this.getAlpha(), @@ -666,9 +541,18 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); + mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const fillFilterExpr = getFillFilterExpression(hasJoins); + if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { + mbMap.setFilter(fillLayerId, fillFilterExpr); + } + this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const lineFilterExpr = getLineFilterExpression(hasJoins); + if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { + mbMap.setFilter(lineLayerId, lineFilterExpr); + } } _syncStylePropertiesWithMb(mbMap) { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js deleted file mode 100644 index 0a07582c57856d..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('./joins/inner_join', () => ({ - InnerJoin: Object -})); - -jest.mock('./tooltips/join_tooltip_property', () => ({ - JoinTooltipProperty: Object -})); - -import { VectorLayer } from './vector_layer'; - -describe('_canSkipSourceUpdate', () => { - const SOURCE_DATA_REQUEST_ID = 'foo'; - - describe('isQueryAware', () => { - - const queryAwareSourceMock = { - isTimeAware: () => { return false; }, - isRefreshTimerAware: () => { return false; }, - isFilterByMapBounds: () => { return false; }, - isFieldAware: () => { return false; }, - isQueryAware: () => { return true; }, - isGeoGridPrecisionAware: () => { return false; }, - }; - const prevFilters = []; - const prevQuery = { - language: 'kuery', - query: 'machine.os.keyword : "win 7"', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' - }; - - describe('applyGlobalQuery is false', () => { - - const prevApplyGlobalQuery = false; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - - describe('applyGlobalQuery is true', () => { - - const prevApplyGlobalQuery = true; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can not skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 2e442c5c61b1e4..2bff760ed3711d 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -5,11 +5,12 @@ */ import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; +import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; export const savedSearchMock = { id: 'the-saved-search-id', title: 'the-saved-search-title', - searchSource: searchSourceMock, + searchSource: searchSourceMock as SearchSourceContract, columns: [], sort: [], destroy: () => {}, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 1caa0686206180..642b4c5649a13d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -176,7 +176,7 @@ export const Page: FC = () => { const searchSource = currentSavedSearch.searchSource; const query = searchSource.getField('query'); if (query !== undefined) { - const queryLanguage = query.language; + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; const qryString = query.query; let qry; if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 0e88b291e76fc9..455fac9b532d61 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -7,7 +7,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; export interface SearchItems { indexPattern: IIndexPattern; @@ -28,7 +28,7 @@ export function createSearchItems( // a lucene query_string. // Using a blank query will cause match_all:{} to be used // when passed through luceneStringToDsl - let query = { + let query: Query = { query: '', language: 'lucene', }; @@ -45,12 +45,12 @@ export function createSearchItems( if (indexPattern.id === undefined && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index')!; - query = searchSource.getField('query'); + query = searchSource.getField('query')!; const fs = searchSource.getField('filter'); - const filters = fs.length ? fs : []; + const filters = Array.isArray(fs) ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index a614be547abde8..aeec71462308e2 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -38,7 +38,7 @@ export function loadNewJobCapabilities( // saved search is being used // load the index pattern from the saved search const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index'); + const indexPattern = savedSearch.searchSource.getField('index')!; await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js index c1425de20d1465..7b300939bd4701 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; -describe('get_all_stats', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_all_stats', () => { const size = 123; const start = 0; const end = 1; diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index e3153670ac58f2..a0072e52fc7f75 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; -describe('get_cluster_uuids', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_cluster_uuids', () => { const callWith = sinon.stub(); const size = 123; const server = { diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts index 9bad3b2b08002a..cb792fbd6ae039 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts @@ -4,88 +4,80 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../test_helpers/create_mock_server'; import { getAbsoluteUrlFactory } from './get_absolute_url'; -test(`by default it builds url using information from server.info.protocol and the server.config`, () => { - const mockServer = createMockServer(''); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); +const defaultOptions = { + defaultBasePath: 'sbp', + protocol: 'http:', + hostname: 'localhost', + port: 5601, +}; + +test(`by default it builds urls using information from server.info.protocol and the server.config`, () => { + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.protocol if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.protocol': 'https', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + protocol: 'https:', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`https://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.hostname if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.hostname': 'something-else', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + hostname: 'something-else', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://something-else:5601/sbp/app/kibana`); }); test(`uses kibanaServer.port if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.port': 8008, - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + port: 8008, + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:8008/sbp/app/kibana`); }); test(`uses the provided hash`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided hash with queryString`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash?querystring'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided basePath`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' }); expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`); }); test(`uses the path`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const path = '/app/canvas'; const absoluteUrl = getAbsoluteUrl({ path }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp${path}`); }); test(`uses the search`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const search = '_t=123456789'; const absoluteUrl = getAbsoluteUrl({ search }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana?${search}`); diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts index 1d34189abcb243..0e350cb1ec0117 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts @@ -5,24 +5,27 @@ */ import url from 'url'; -import { ServerFacade } from '../types'; - -export function getAbsoluteUrlFactory(server: ServerFacade) { - const config = server.config(); +import { AbsoluteURLFactoryOptions } from '../types'; +export const getAbsoluteUrlFactory = ({ + protocol, + hostname, + port, + defaultBasePath, +}: AbsoluteURLFactoryOptions) => { return function getAbsoluteUrl({ - basePath = config.get('server.basePath'), + basePath = defaultBasePath, hash = '', path = '/app/kibana', search = '', } = {}) { return url.format({ - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + protocol, + hostname, + port, pathname: basePath + path, hash, search, }); }; -} +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index 3b828520734217..2b66c77067ed2e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -28,7 +28,14 @@ export async function getFullUrls({ job: JobDocPayloadPNG | JobDocPayloadPDF; server: ServerFacade; }) { - const getAbsoluteUrl = getAbsoluteUrlFactory(server); + const config = server.config(); + + const getAbsoluteUrl = getAbsoluteUrlFactory({ + defaultBasePath: config.get('server.basePath'), + protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, + hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), + port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + }); // PDF and PNG job params put in the url differently let relativeUrls: string[] = []; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js index 195a0d4fdbec45..6d638e50af4762 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../../common/cancellation_token'; -describe('CancellationToken', function () { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('CancellationToken', function () { let cancellationToken; beforeEach(function () { cancellationToken = new CancellationToken(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 84549d0680ff36..b2e87482b73a1e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -26,6 +26,7 @@ const defaultWorkerOptions = { intervalErrorMultiplier: 10 }; + describe('Worker class', function () { // some of these tests might be a little slow, give them a little extra time this.timeout(10000); @@ -1068,7 +1069,8 @@ describe('Format Job Object', () => { }); }); -describe('Get Doc Path from ES Response', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function () { const responseMock = { _index: 'foo', diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js index 9a74ba63b8e312..8b5d6f4591ff5f 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { validateConfig } from '../validate_config'; -describe('Reporting: Validate config', () => { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('Reporting: Validate config', () => { const logger = { warning: sinon.spy(), }; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js index 10ff9f477f4248..59317ac46773ba 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js @@ -6,11 +6,10 @@ import boom from 'boom'; import { getUserFactory } from '../../lib/get_user'; -import { oncePerServer } from '../../lib/once_per_server'; const superuserRole = 'superuser'; -function authorizedUserPreRoutingFn(server) { +export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn(server) { const getUser = getUserFactory(server); const config = server.config(); @@ -40,6 +39,4 @@ function authorizedUserPreRoutingFn(server) { return user; }; -} - -export const authorizedUserPreRoutingFactory = oncePerServer(authorizedUserPreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js index ad91e5a654a4e4..92973e3d0b4220 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js @@ -5,9 +5,8 @@ */ import Boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; -function reportingFeaturePreRoutingFn(server) { +export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn(server) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'reporting'; @@ -24,6 +23,4 @@ function reportingFeaturePreRoutingFn(server) { } }; }; -} - -export const reportingFeaturePreRoutingFactory = oncePerServer(reportingFeaturePreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 7d05811ef4aa64..e8fb015426f515 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -326,3 +326,10 @@ export { CancellationToken } from './common/cancellation_token'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger } from './server/lib/level_logger'; + +export interface AbsoluteURLFactoryOptions { + defaultBasePath: string; + protocol: string; + hostname: string; + port: string | number; +} diff --git a/x-pack/legacy/plugins/rollup/public/search/register.js b/x-pack/legacy/plugins/rollup/public/search/register.js index 917ee872254f56..f7f1c681b63caa 100644 --- a/x-pack/legacy/plugins/rollup/public/search/register.js +++ b/x-pack/legacy/plugins/rollup/public/search/register.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { addSearchStrategy } from 'ui/courier'; +import { addSearchStrategy } from '../../../../../../src/legacy/ui/public/courier'; import { rollupSearchStrategy } from './rollup_search_strategy'; export function initSearch() { diff --git a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js index ab24a37a2ecec4..28f08ba1ab952f 100644 --- a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js @@ -5,7 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { SearchError, getSearchErrorType } from 'ui/courier'; +import { SearchError, getSearchErrorType } from '../../../../../../src/legacy/ui/public/courier'; function serializeFetchParams(searchRequests) { return JSON.stringify(searchRequests.map(searchRequestWithFetchParams => { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx index bf27620dcac181..d709a8feb48bd4 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx @@ -15,13 +15,12 @@ export const EmptyTreePlaceHolder = () => { {/* TODO: translations */}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeTitle', { - defaultMessage: 'Nothing to see here yet.', + defaultMessage: 'No queries to profile', })}

{i18n.translate('xpack.searchProfiler.emptyProfileTreeDescription', { - defaultMessage: - 'Enter a query and press the "Profile" button or provide profile data in the editor.', + defaultMessage: 'Enter a query, click Profile, and see the results here.', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx index fb09c6cddf70a4..a7db54b670a846 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx @@ -13,7 +13,7 @@ export const ProfileLoadingPlaceholder = () => {

{i18n.translate('xpack.searchProfiler.profilingLoaderText', { - defaultMessage: 'Profiling...', + defaultMessage: 'Loading query profiles...', })}

diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx index 7f5d223949e610..63ae5c7583625a 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx @@ -93,7 +93,7 @@ export const Main = () => { return ( <> - + {renderLicenseWarning()} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts index dac9dab9bd092f..615511786afd15 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts @@ -12,7 +12,6 @@ import { OnHighlightChangeArgs } from '../components/profile_tree'; import { ShardSerialized, Targets } from '../types'; export type Action = - | { type: 'setPristine'; value: boolean } | { type: 'setProfiling'; value: boolean } | { type: 'setHighlightDetails'; value: OnHighlightChangeArgs | null } | { type: 'setActiveTab'; value: Targets | null } @@ -20,12 +19,8 @@ export type Action = export const reducer: Reducer = (state, action) => produce(state, draft => { - if (action.type === 'setPristine') { - draft.pristine = action.value; - return; - } - if (action.type === 'setProfiling') { + draft.pristine = false; draft.profiling = action.value; if (draft.profiling) { draft.currentResponse = null; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts index 7b5a1ce93583d6..7008854a162858 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts @@ -18,7 +18,7 @@ export interface State { export const initialState: State = { profiling: false, - pristine: false, + pristine: true, highlightDetails: null, activeTab: null, currentResponse: null, diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss index a72d079354f897..d36a587b9257f5 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -10,12 +10,6 @@ @import 'containers/main'; @import 'containers/profile_query_editor'; -#searchProfilerAppRoot { - height: 100%; - display: flex; - flex: 1 1 auto; -} - .prfDevTool__licenseWarning { &__container { max-width: 1000px; @@ -55,19 +49,10 @@ } } -.prfDevTool { - height: calc(100vh - #{$euiHeaderChildSize}); +.appRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); overflow: hidden; - - .devApp__container { - height: 100%; - overflow: hidden; - flex-shrink: 1; - } - - &__container { - overflow: hidden; - } + flex-shrink: 1; } .prfDevTool__detail { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss index cc4d334f58fd33..c7dc4a305acb20 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss @@ -5,10 +5,6 @@ $badgeSize: $euiSize * 5.5; .prfDevTool__profileTree { - &__container { - height: 100%; - } - &__shardDetails--dim small { color: $euiColorDarkShade; } diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index c098e3e67a6d91..60374d562f96c5 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -29,8 +29,12 @@ export const security = (kibana) => new kibana.Plugin({ enabled: Joi.boolean().default(true), cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + session: Joi.object({ + idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -43,9 +47,10 @@ export const security = (kibana) => new kibana.Plugin({ }).default(); }, - deprecations: function ({ unused }) { + deprecations: function ({ rename, unused }) { return [ unused('authorization.legacyFallback.enabled'), + rename('sessionTimeout', 'session.idleTimeout'), ]; }, @@ -88,7 +93,11 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: securityPlugin.__legacyCompat.config.secureCookies, - sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, + session: { + tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, + idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, + lifespan: securityPlugin.__legacyCompat.config.session.lifespan, + }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, @@ -147,7 +156,9 @@ export const security = (kibana) => new kibana.Plugin({ server.injectUiAppVars('login', () => { const { showLogin, allowLogin, layout = 'form' } = securityPlugin.__legacyCompat.license.getFeatures(); + const { loginAssistanceMessage } = securityPlugin.__legacyCompat.config; return { + loginAssistanceMessage, loginState: { showLogin, allowLogin, diff --git a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js index 81b14ee7d8bf41..d9fb4507794113 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js @@ -7,28 +7,20 @@ import _ from 'lodash'; import { uiModules } from 'ui/modules'; import { isSystemApiRequest } from 'ui/system_api'; -import { Path } from 'plugins/xpack_main/services/path'; import { npSetup } from 'ui/new_platform'; -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - * @type {number} - */ - const module = uiModules.get('security', []); module.config(($httpProvider) => { $httpProvider.interceptors.push(( $q, ) => { - const isUnauthenticated = Path.isUnauthenticated(); + const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname); function interceptorFactory(responseHandler) { return function interceptor(response) { - if (!isUnauthenticated && !isSystemApiRequest(response.config)) { - npSetup.plugins.security.sessionTimeout.extend(); + if (!isAnonymous && !isSystemApiRequest(response.config)) { + npSetup.plugins.security.sessionTimeout.extend(response.config.url); } return responseHandler(response); }; diff --git a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx index 369b531e8ddf80..dbeb68875c1a9b 100644 --- a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx +++ b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx @@ -31,7 +31,7 @@ chrome } > - + , diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 3b3024024a9cf1..a08c454e569e6d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -2,6 +2,20 @@ exports[`BasicLoginForm renders as expected 1`] = ` + + +
{ loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ) ).toMatchSnapshot(); @@ -68,6 +69,7 @@ describe('BasicLoginForm', () => { next={''} infoMessage={'Hey this is an info message'} intl={null as any} + loginAssistanceMessage="" /> ); @@ -86,6 +88,7 @@ describe('BasicLoginForm', () => { loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index 9dbb556f5f5f45..acdc29842d4c65 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -7,6 +7,8 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { EuiText } from '@elastic/eui'; import { LoginState } from '../../../../../common/login_state'; interface Props { @@ -16,6 +18,7 @@ interface Props { loginState: LoginState; next: string; intl: InjectedIntl; + loginAssistanceMessage: string; } interface State { @@ -38,6 +41,7 @@ class BasicLoginFormUI extends Component { public render() { return ( + {this.renderLoginAssistanceMessage()} {this.renderMessage()} @@ -102,6 +106,16 @@ class BasicLoginFormUI extends Component { ); } + private renderLoginAssistanceMessage = () => { + return ( + + + {this.props.loginAssistanceMessage} + + + ); + }; + private renderMessage = () => { if (this.state.message) { return ( @@ -132,6 +146,7 @@ class BasicLoginFormUI extends Component { ); } + return null; }; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap index fc33c6e0a82cc2..17ba81988414ae 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap @@ -160,6 +160,88 @@ exports[`LoginPage disabled form states renders as expected when an unknown logi
`; +exports[`LoginPage disabled form states renders as expected when loginAssistanceMessage is set 1`] = ` +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + +
+
+`; + exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
{ loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: true, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -61,6 +62,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -76,6 +78,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -91,6 +94,21 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', + }; + + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when loginAssistanceMessage is set', () => { + const props = { + http: createMockHttp(), + window: {}, + next: '', + loginState: createLoginState(), + isSecureConnection: false, + requiresSecureConnection: false, + loginAssistanceMessage: 'This is an *important* message', }; expect(shallow()).toMatchSnapshot(); @@ -106,6 +124,7 @@ describe('LoginPage', () => { loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index 82dd0e679a5eef..e7e56947ca58f8 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -31,6 +31,7 @@ interface Props { loginState: LoginState; isSecureConnection: boolean; requiresSecureConnection: boolean; + loginAssistanceMessage: string; } export class LoginPage extends Component { diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index 8b452e4c4fdf53..d9daf2d1f4d0de 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -39,7 +39,8 @@ interface AnyObject { $http: AnyObject, $window: AnyObject, secureCookies: boolean, - loginState: LoginState + loginState: LoginState, + loginAssistanceMessage: string ) => { const basePath = chrome.getBasePath(); const next = parseNext($window.location.href, basePath); @@ -59,6 +60,7 @@ interface AnyObject { loginState={loginState} isSecureConnection={isSecure} requiresSecureConnection={secureCookies} + loginAssistanceMessage={loginAssistanceMessage} next={next} /> , diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx index 282ce4eea160cb..91f5f048adc6d0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx @@ -374,7 +374,7 @@ class EditUserPageUI extends Component { -

+

{isNewUser ? ( { values={{ userName: user.username }} /> )} -

+
{reserved && ( diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index f01493cec869e5..e5d1fc83dac264 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -17,6 +17,8 @@ export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts index 132242606d88cc..7a6c7f71bc98c2 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOGOUT } from '../urls'; - export const logout = (): null => { - cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`); + cy.request({ + method: 'GET', + url: `${Cypress.config().baseUrl}/logout`, + }).then(response => { + expect(response.status).to.eq(200); + }); return null; }; diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js similarity index 70% rename from x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js rename to x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 597d93a44210bc..3e1c5f51ebb5c8 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -11,22 +11,22 @@ const path = require('path'); /* * This script is used to parse a set of saved searches on a file system - * and output signal data compatible json files. + * and output rule data compatible json files. * Example: - * node saved_query_to_signals.js ${HOME}/saved_searches ${HOME}/saved_signals + * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules * - * After editing any changes in the files of ${HOME}/saved_signals/*.json - * you can then post the signals with a CURL post script such as: + * After editing any changes in the files of ${HOME}/saved_rules/*.json + * you can then post the rules with a CURL post script such as: * - * ./post_signal.sh ${HOME}/saved_signals/*.json + * ./post_rule.sh ${HOME}/saved_rules/*.json * * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved signals. + * when it outputs the saved rules. */ -// Defaults of the outputted signals since the saved KQL searches do not have +// Defaults of the outputted rules since the saved KQL searches do not have // this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a signal or checking it +// doing a search to KQL conversion before posting it as a rule or checking it // into another repository. const INTERVAL = '5m'; const SEVERITY = 'low'; @@ -34,9 +34,17 @@ const TYPE = 'query'; const FROM = 'now-6m'; const TO = 'now'; const IMMUTABLE = true; -const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const RISK_SCORE = 50; +const ENABLED = false; +let allRules = ''; +const allRulesNdJson = 'all_rules.ndjson'; + +// For converting, if you want to use these instead of rely on the defaults then +// comment these in and use them for the script. Otherwise this is commented out +// so we can utilize the defaults of input and output which are based on saved objects +// of siem:defaultIndex and siem:defaultSignalsIndex +// const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; +// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const walk = dir => { const list = fs.readdirSync(dir); @@ -66,7 +74,7 @@ const cleanupFileName = file => { async function main() { if (process.argv.length !== 4) { throw new Error( - 'usage: saved_query_to_signals [input directory with saved searches] [output directory]' + 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' ); } @@ -124,7 +132,6 @@ async function main() { risk_score: RISK_SCORE, description: description || title, immutable: IMMUTABLE, - index: INDEX, interval: INTERVAL, name: title, severity: SEVERITY, @@ -134,16 +141,22 @@ async function main() { query, language, filters: filter, - output_index: OUTPUT_INDEX, + enabled: ENABLED, + // comment these in if you want to use these for input output, otherwise + // with these two commented out, we will use the default saved objects from spaces. + // index: INDEX, + // output_index: OUTPUT_INDEX, }; fs.writeFileSync( `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); + allRules += `${JSON.stringify(outputMessage)}\n`; } } ); + fs.writeFileSync(`${outputDir}/${allRulesNdJson}`, allRules); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index c79b2651c11cb1..2f1530a777042a 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,13 +15,13 @@ import { timelineSavedObjectType, } from './saved_objects'; -import { signalsAlertType } from './lib/detection_engine/alerts/signals_alert_type'; +import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createSignalsRoute } from './lib/detection_engine/routes/create_signals_route'; -import { readSignalsRoute } from './lib/detection_engine/routes/read_signals_route'; -import { findSignalsRoute } from './lib/detection_engine/routes/find_signals_route'; -import { deleteSignalsRoute } from './lib/detection_engine/routes/delete_signals_route'; -import { updateSignalsRoute } from './lib/detection_engine/routes/update_signals_route'; +import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; const APP_ID = 'siem'; @@ -32,7 +32,8 @@ export const initServerWithKibana = ( mode: EnvironmentMode ) => { if (kbnServer.plugins.alerting != null) { - const type = signalsAlertType({ logger }); + const version = kbnServer.config().get('pkg.version'); + const type = rulesAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } @@ -48,13 +49,13 @@ export const initServerWithKibana = ( kbnServer.config().has('xpack.alerting.enabled') === true ) { logger.info( - 'Detected feature flags for actions and alerting and enabling signals API endpoints' + 'Detected feature flags for actions and alerting and enabling detection engine API endpoints' ); - createSignalsRoute(kbnServer); - readSignalsRoute(kbnServer); - updateSignalsRoute(kbnServer); - deleteSignalsRoute(kbnServer); - findSignalsRoute(kbnServer); + createRulesRoute(kbnServer); + readRulesRoute(kbnServer); + updateRulesRoute(kbnServer); + deleteRulesRoute(kbnServer); + findRulesRoute(kbnServer); } const xpackMainPlugin = kbnServer.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 0a0439a9ace1b0..4b1dbf62d0dd4c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -24,10 +24,10 @@ xpack.alerting.enabled: true xpack.actions.enabled: true ``` -Start Kibana and you will see these messages indicating signals is activated like so: +Start Kibana and you will see these messages indicating detection engine is activated like so: ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` If you see crashes like this: @@ -98,10 +98,17 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status chan server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -You should also see the SIEM detect the feature flags and start the API endpoints for signals +You should also see the SIEM detect the feature flags and start the API endpoints for detection engine ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints +``` + +Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same +value as you did with the environment variable of SIGNALS_INDEX, which should be `.siem-signals-${your user id}` + +``` +.siem-signals-${your user id} ``` Open a terminal and go into the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: @@ -118,16 +125,16 @@ which will: - Delete any existing alert tasks you have - Delete any existing signal mapping you might have had. - Add the latest signal index and its mappings using your settings from `SIGNALS_INDEX` environment variable. -- Posts the sample signal from `signals/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable -- The sample signal checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit +- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run ```sh -./find_signals.sh +./find_rules.sh ``` -You should see the new signals created like so: +You should see the new rules created like so: ```sh { @@ -177,7 +184,7 @@ Every 5 minutes if you get positive hits you will see messages on info like so: server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` -Signals are space aware and default to the "default" space for these scripts if you do not export +Rules are space aware and default to the "default" space for these scripts if you do not export the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would set your SPACE_URL to be: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 0a70a7342b2dd3..079d3658461fa5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, AlertTypeParams } from '../types'; +import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; -export const sampleSignalAlertParams = ( +export const sampleRuleAlertParams = ( maxSignals: number | undefined, riskScore?: number | undefined -): AlertTypeParams => ({ +): RuleTypeParams => ({ ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], @@ -30,33 +30,43 @@ export const sampleSignalAlertParams = ( filters: undefined, savedId: undefined, meta: undefined, - size: 1000, }); -export const sampleDocNoSortId: SignalSourceHit = { +export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, -}; +}); + +export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _id: someUuid, + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, +}); -export const sampleDocWithSortId: SignalSourceHit = { +export const sampleDocWithSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, sort: ['1234567891111'], -}; +}); export const sampleEmptyDocSearchResults: SignalSearchResponse = { took: 10, @@ -74,7 +84,61 @@ export const sampleEmptyDocSearchResults: SignalSearchResponse = { }, }; -export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { +export const sampleBulkCreateDuplicateResult = { + took: 60, + errors: true, + items: [ + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + _version: 1, + result: 'created', + _shards: { + total: 2, + successful: 1, + failed: 0, + }, + _seq_no: 1, + _primary_term: 1, + status: 201, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + ], +}; + +export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -88,13 +152,35 @@ export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); + +export const sampleDocSearchResultsNoSortIdNoVersion = ( + someUuid: string +): SignalSearchResponse => ({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 100, + max_score: 100, + hits: [ + { + ...sampleDocNoSortIdNoVersion(someUuid), + }, + ], + }, +}); -export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { +export const sampleDocSearchResultsNoSortIdNoHits = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -108,13 +194,17 @@ export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); -export const repeatedSearchResultsWithSortId = (repeat: number) => ({ +export const repeatedSearchResultsWithSortId = ( + total: number, + pageSize: number, + guids: string[] +) => ({ took: 10, timed_out: false, _shards: { @@ -124,15 +214,15 @@ export const repeatedSearchResultsWithSortId = (repeat: number) => ({ skipped: 0, }, hits: { - total: repeat, + total, max_score: 100, - hits: Array.from({ length: repeat }).map(x => ({ - ...sampleDocWithSortId, + hits: Array.from({ length: pageSize }).map((x, index) => ({ + ...sampleDocWithSortId(guids[index]), })), }, }); -export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { +export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -146,10 +236,10 @@ export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocWithSortId, + ...sampleDocWithSortId(someUuid), }, ], }, -}; +}); -export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts index 420f9954314232..7c66714484383f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -5,9 +5,9 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { SignalParams } from './types'; +import { RuleParams } from './types'; -export const createSignals = async ({ +export const createRules = async ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... description, @@ -29,12 +29,11 @@ export const createSignals = async ({ outputIndex, name, severity, - size, tags, to, type, references, -}: SignalParams) => { +}: RuleParams) => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts similarity index 65% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts index d89895772f1efc..c3ca1d79424cf2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { readSignals } from './read_signals'; -import { DeleteSignalParams } from './types'; +import { readRules } from './read_rules'; +import { DeleteRuleParams } from './types'; -export const deleteSignals = async ({ +export const deleteRules = async ({ alertsClient, actionsClient, // TODO: Use this when we have actions such as email, etc... id, ruleId, -}: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id, ruleId }); - if (signal == null) { +}: DeleteRuleParams) => { + const rule = await readRules({ alertsClient, id, ruleId }); + if (rule == null) { return null; } if (ruleId != null) { - await alertsClient.delete({ id: signal.id }); - return signal; + await alertsClient.delete({ id: rule.id }); + return rule; } else if (id != null) { try { await alertsClient.delete({ id }); - return signal; + return rule; } catch (err) { if (err.output.statusCode === 404) { return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts index 7873781fb05c47..23f031b22a9dd8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFilter } from './find_signals'; +import { getFilter } from './find_rules'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { test('it returns a full filter with an AND if sent down', () => { expect(getFilter('alert.attributes.enabled: true')).toEqual( `alert.attributes.alertTypeId: ${SIGNALS_ID} AND alert.attributes.enabled: true` diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts index 63e6a069c0cfe1..c1058bd353e8ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts @@ -5,7 +5,7 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { FindSignalParams } from './types'; +import { FindRuleParams } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -15,7 +15,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findSignals = async ({ +export const findRules = async ({ alertsClient, perPage, page, @@ -23,7 +23,7 @@ export const findSignals = async ({ filter, sortField, sortOrder, -}: FindSignalParams) => { +}: FindRuleParams) => { return alertsClient.find({ options: { fields, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index 1aa22ea024cc85..5d3b47ecebfd51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalAlertParams, PartialFilter } from './types'; +import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -41,7 +41,7 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: SignalAlertParams['type']; + type: RuleAlertParams['type']; filter: Record | undefined | null; filters: PartialFilter[] | undefined | null; language: string | undefined | null; @@ -86,7 +86,7 @@ export const getFilter = async ({ if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index); } else { - // user did not give any additional fall back mechanism for generating a signal + // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring throw err; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts new file mode 100644 index 00000000000000..07eb7c885b4435 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; + +describe('get_input_output_index', () => { + let savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + let servicesMock: AlertServices = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + servicesMock = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + }); + + describe('getInputOutputIndex', () => { + test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + null + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + null, + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns a saved object outputIndex if both passed in are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + 'output-index-1' + ); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); + + describe('getOutputIndex', () => { + test('test output index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex('output-index-1', mockConfiguration); + expect(outputIndex).toEqual('output-index-1'); + }); + + test('configured output index is returned when output index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual('.siem-test-signals'); + }); + + test('output index from constants is returned when output index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + }); + + describe('getInputIndex', () => { + test('test input index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); + expect(inputIndex).toEqual(['input-index-1']); + }); + + test('configured input index is returned when input index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); + }); + + test('input index from constants is returned when input index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts new file mode 100644 index 00000000000000..567ab27976d8d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; + +interface IndexObjectAttributes extends SavedObjectAttributes { + [DEFAULT_INDEX_KEY]: string[]; + [DEFAULT_SIGNALS_INDEX_KEY]: string; +} + +export const getInputIndex = ( + inputIndex: string[] | undefined | null, + configuration: SavedObject +): string[] => { + if (inputIndex != null) { + return inputIndex; + } else { + if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { + return configuration.attributes[DEFAULT_INDEX_KEY]; + } else { + return defaultIndexPattern; + } + } +}; + +export const getOutputIndex = ( + outputIndex: string | undefined | null, + configuration: SavedObject +): string => { + if (outputIndex != null) { + return outputIndex; + } else { + if ( + configuration.attributes != null && + configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null + ) { + return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; + } else { + return DEFAULT_SIGNALS_INDEX; + } + } +}; + +export const getInputOutputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined, + outputIndex: string | null | undefined +): Promise<{ + inputIndex: string[]; + outputIndex: string; +}> => { + if (inputIndex != null && outputIndex != null) { + return { inputIndex, outputIndex }; + } else { + const configuration = await services.savedObjectsClient.get('config', version); + return { + inputIndex: getInputIndex(inputIndex, configuration), + outputIndex: getOutputIndex(outputIndex, configuration), + }; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts index 39d1fac8f7a09f..b3d7ab13227750 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; +import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; import { getResult, @@ -14,19 +14,19 @@ import { } from '../routes/__mocks__/request_responses'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('read_signals', () => { - describe('readSignals', () => { +describe('read_rules', () => { + describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { @@ -34,12 +34,12 @@ describe('read_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { @@ -48,12 +48,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is null but ruleId is set', async () => { @@ -62,12 +62,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return null if id and ruleId are null', async () => { @@ -76,12 +76,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: null, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return null if id and ruleId are undefined', async () => { @@ -90,27 +90,27 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: undefined, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('readSignalByRuleId', () => { + describe('readRuleByRuleId', () => { test('should return a single value if the rule id matches', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should not return a single value if the rule id does not match', async () => { @@ -119,11 +119,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return a single value of rule-1 with multiple values', async () => { @@ -140,11 +140,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(result1); + expect(rule).toEqual(result1); }); test('should return a single value of rule-2 with multiple values', async () => { @@ -161,11 +161,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-2', }); - expect(signal).toEqual(result2); + expect(rule).toEqual(result2); }); test('should return null for a made up value with multiple values', async () => { @@ -182,57 +182,57 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('findSignalInArrayByRuleId', () => { + describe('findRuleInArrayByRuleId', () => { test('returns null if the objects are not of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: 'made up 1', params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('returns correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts similarity index 50% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts index 3c49112aaf50b5..5c335263290163 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; +import { findRules } from './find_rules'; +import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; -export const findSignalInArrayByRuleId = ( +export const findRuleInArrayByRuleId = ( objects: object[], ruleId: string -): SignalAlertType | null => { +): RuleAlertType | null => { if (isAlertTypeArray(objects)) { - const signals: SignalAlertType[] = objects; - const signal: SignalAlertType[] = signals.filter(datum => { + const rules: RuleAlertType[] = objects; + const rule: RuleAlertType[] = rules.filter(datum => { return datum.params.ruleId === ruleId; }); - if (signal.length !== 0) { - return signal[0]; + if (rule.length !== 0) { + return rule[0]; } else { return null; } @@ -26,32 +26,32 @@ export const findSignalInArrayByRuleId = ( } }; -// This an extremely slow and inefficient way of getting a signal by its id. -// I have to manually query every single record since the Signal Params are +// This an extremely slow and inefficient way of getting a rule by its id. +// I have to manually query every single record since the rule Params are // not indexed and I cannot push in my own _id when I create an alert at the moment. // TODO: Once we can directly push in the _id, then we should no longer need this way. // TODO: This is meant to be _very_ temporary. -export const readSignalByRuleId = async ({ +export const readRuleByRuleId = async ({ alertsClient, ruleId, -}: ReadSignalByRuleId): Promise => { - const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); - if (firstSignal != null) { - return firstSignal; +}: ReadRuleByRuleId): Promise => { + const firstRules = await findRules({ alertsClient, page: 1 }); + const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); + if (firstRule != null) { + return firstRule; } else { - const totalPages = Math.ceil(firstSignals.total / firstSignals.perPage); + const totalPages = Math.ceil(firstRules.total / firstRules.perPage); return Array(totalPages) .fill({}) .map((_, page) => { // page index never starts at zero. It always has to be 1 or greater - return findSignals({ alertsClient, page: page + 1 }); + return findRules({ alertsClient, page: page + 1 }); }) - .reduce>(async (accum, findSignal) => { - const signals = await findSignal; - const signal = findSignalInArrayByRuleId(signals.data, ruleId); - if (signal != null) { - return signal; + .reduce>(async (accum, findRule) => { + const rules = await findRule; + const rule = findRuleInArrayByRuleId(rules.data, ruleId); + if (rule != null) { + return rule; } else { return accum; } @@ -59,7 +59,7 @@ export const readSignalByRuleId = async ({ } }; -export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { +export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { if (id != null) { try { const output = await alertsClient.get({ id }); @@ -73,7 +73,7 @@ export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams } } } else if (ruleId != null) { - return readSignalByRuleId({ alertsClient, ruleId }); + return readRuleByRuleId({ alertsClient, ruleId }); } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts similarity index 71% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 8308bca68e9af5..91d7d18a4945cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -6,14 +6,25 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; -import { SIGNALS_ID } from '../../../../common/constants'; +import { + SIGNALS_ID, + DEFAULT_MAX_SIGNALS, + DEFAULT_SEARCH_AFTER_PAGE_SIZE, +} from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; -import { searchAfterAndBulkIndex } from './utils'; -import { SignalAlertTypeDefinition } from './types'; +import { searchAfterAndBulkCreate } from './utils'; +import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; +import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTypeDefinition => { +export const rulesAlertType = ({ + logger, + version, +}: { + logger: Logger; + version: string; +}): RuleAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -26,21 +37,20 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp filter: schema.nullable(schema.object({}, { allowUnknowns: true })), ruleId: schema.string(), immutable: schema.boolean({ defaultValue: false }), - index: schema.arrayOf(schema.string()), + index: schema.nullable(schema.arrayOf(schema.string())), language: schema.nullable(schema.string()), - outputIndex: schema.string(), + outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: 10000 }), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), riskScore: schema.number(), severity: schema.string(), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), - size: schema.maybe(schema.number()), }), }, async executor({ alertId, services, params }) { @@ -56,7 +66,6 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp query, to, type, - size, } = params; // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 @@ -68,8 +77,18 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const interval: string = savedObject.attributes.interval; const enabled: boolean = savedObject.attributes.enabled; - const searchAfterSize = size ? size : 1000; + // set searchAfter page size to be the lesser of default page size or maxSignals. + const searchAfterSize = + DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals + ? DEFAULT_SEARCH_AFTER_PAGE_SIZE + : params.maxSignals; + const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( + services, + version, + index, + outputIndex + ); const esFilter = await getFilter({ type, filter, @@ -78,11 +97,11 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp query, savedId, services, - index, + index: inputIndex, }); const noReIndex = buildEventsSearchQuery({ - index, + index: inputIndex, from, to, filter: esFilter, @@ -98,22 +117,27 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const noReIndexResult = await services.callCluster('search', noReIndex); if (noReIndexResult.hits.total.value !== 0) { logger.info( - `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}": ${noReIndexResult.hits.total.value}` + `Found ${ + noReIndexResult.hits.total.value + } signals from the indexes of "${inputIndex.join( + ', ' + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` ); } - const bulkIndexResult = await searchAfterAndBulkIndex({ + const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, - signalParams: params, + ruleParams: params, services, logger, id: alertId, - signalsIndex: outputIndex, + signalsIndex, name, createdBy, updatedBy, interval, enabled, + pageSize: searchAfterSize, }); if (bulkIndexResult) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 9c6e1f99c672b2..28431b81652660 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,7 +21,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; -export interface SignalAlertParams { +export interface RuleAlertParams { description: string; enabled: boolean; falsePositives: string[]; @@ -42,37 +42,36 @@ export interface SignalAlertParams { savedId: string | undefined | null; meta: Record | undefined | null; severity: string; - size: number | undefined | null; tags: string[]; to: string; type: 'filter' | 'query' | 'saved_query'; } -export type SignalAlertParamsRest = Omit< - SignalAlertParams, +export type RuleAlertParamsRest = Omit< + RuleAlertParams, 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' > & { - rule_id: SignalAlertParams['ruleId']; - false_positives: SignalAlertParams['falsePositives']; - saved_id: SignalAlertParams['savedId']; - max_signals: SignalAlertParams['maxSignals']; - risk_score: SignalAlertParams['riskScore']; - output_index: SignalAlertParams['outputIndex']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; }; -export type OutputSignalAlertRest = SignalAlertParamsRest & { +export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; }; -export type OutputSignalES = OutputSignalAlertRest & { +export type OutputRuleES = OutputRuleAlertRest & { status: 'open' | 'closed'; }; -export type UpdateSignalAlertParamsRest = Partial & { +export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; - rule_id: SignalAlertParams['ruleId'] | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { @@ -89,18 +88,18 @@ export interface Clients { actionsClient: ActionsClient; } -export type SignalParams = SignalAlertParams & Clients; +export type RuleParams = RuleAlertParams & Clients; -export type UpdateSignalParams = Partial & { +export type UpdateRuleParams = Partial & { id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { +export type DeleteRuleParams = Clients & { id: string | undefined; ruleId: string | undefined | null; }; -export interface FindSignalsRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -112,7 +111,7 @@ export interface FindSignalsRequest extends Omit { }; } -export interface FindSignalParams { +export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; page?: number; @@ -122,34 +121,34 @@ export interface FindSignalParams { sortOrder?: 'asc' | 'desc'; } -export interface ReadSignalParams { +export interface ReadRuleParams { alertsClient: AlertsClient; id?: string | undefined | null; ruleId?: string | undefined | null; } -export interface ReadSignalByRuleId { +export interface ReadRuleByRuleId { alertsClient: AlertsClient; ruleId: string; } -export type AlertTypeParams = Omit; +export type RuleTypeParams = Omit; -export type SignalAlertType = Alert & { +export type RuleAlertType = Alert & { id: string; - params: AlertTypeParams; + params: RuleTypeParams; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalAlertParamsRest; +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; } -export interface UpdateSignalsRequest extends RequestFacade { - payload: UpdateSignalAlertParamsRest; +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; } -export type SignalExecutorOptions = Omit & { - params: SignalAlertParams & { +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { scrollSize: number; scrollLock: string; }; @@ -173,7 +172,46 @@ export interface SignalSource { export interface BulkResponse { took: number; errors: boolean; - items: unknown[]; + items: [ + { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; + } + ]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; } export type SignalSearchResponse = SearchResponse; @@ -183,24 +221,24 @@ export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -// This returns true because by default a SignalAlertTypeDefinition is an AlertType +// This returns true because by default a RuleAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { +export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { return true; }; -export type SignalAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: SignalExecutorOptions) => Promise; +export type RuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; }; -export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { - return obj.every(signal => isAlertType(signal)); +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is SignalAlertType => { +export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isAlertTypeArray = (objArray: unknown[]): objArray is SignalAlertType[] => { +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { return objArray.length === 0 || isAlertType(objArray[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts index 39f7951a8eab98..1022fea93200fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName } from './update_signals'; +import { calculateInterval, calculateName } from './update_rules'; -describe('update_signals', () => { +describe('update_rules', () => { describe('#calculateInterval', () => { - test('given a undefined interval, it returns the signalInterval ', () => { + test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); expect(interval).toEqual('10m'); }); - test('given a undefined signalInterval, it returns a undefined interval ', () => { + test('given a undefined ruleInterval, it returns a undefined interval ', () => { const interval = calculateInterval('10m', undefined); expect(interval).toEqual('10m'); }); - test('given both an undefined signalInterval and a undefined interval, it returns 5m', () => { + test('given both an undefined ruleInterval and a undefined interval, it returns 5m', () => { const interval = calculateInterval(undefined, undefined); expect(interval).toEqual('5m'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts similarity index 66% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts index a38fd7756afa14..81360d78242302 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -6,17 +6,17 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; -import { readSignals } from './read_signals'; -import { UpdateSignalParams } from './types'; +import { readRules } from './read_rules'; +import { UpdateRuleParams } from './types'; export const calculateInterval = ( interval: string | undefined, - signalInterval: string | undefined + ruleInterval: string | undefined ): string => { if (interval != null) { return interval; - } else if (signalInterval != null) { - return signalInterval; + } else if (ruleInterval != null) { + return ruleInterval; } else { return '5m'; } @@ -35,13 +35,13 @@ export const calculateName = ({ return originalName; } else { // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a signal rule name became null or undefined at + // the name of "untitled" just in case a rule name became null or undefined at // some point since TypeScript allows it. return 'untitled'; } }; -export const updateSignal = async ({ +export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types description, @@ -68,17 +68,17 @@ export const updateSignal = async ({ to, type, references, -}: UpdateSignalParams) => { - const signal = await readSignals({ alertsClient, ruleId, id }); - if (signal == null) { +}: UpdateRuleParams) => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { return null; } - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed + // TODO: Remove this as cast as soon as rule.actions TypeScript bug is fixed // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; + const actions = (rule.actions as AlertAction[] | undefined) || []; - const params = signal.params || {}; + const params = rule.params || {}; const nextParams = defaults( { @@ -107,18 +107,18 @@ export const updateSignal = async ({ } ); - if (signal.enabled && !enabled) { - await alertsClient.disable({ id: signal.id }); - } else if (!signal.enabled && enabled) { - await alertsClient.enable({ id: signal.id }); + if (rule.enabled && !enabled) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled) { + await alertsClient.enable({ id: rule.id }); } return alertsClient.update({ - id: signal.id, + id: rule.id, data: { tags: [], - name: calculateName({ updatedName: name, originalName: signal.name }), - interval: calculateInterval(interval, signal.interval), + name: calculateName({ updatedName: name, originalName: rule.name }), + interval: calculateInterval(interval, rule.interval), actions, params: nextParams, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index bc147fa1dae07c..19c8d5ccc87ca2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -3,24 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import uuid from 'uuid'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { Logger } from '../../../../../../../../src/core/server'; import { buildBulkBody, - singleBulkIndex, + generateId, + singleBulkCreate, singleSearchAfter, - searchAfterAndBulkIndex, + searchAfterAndBulkCreate, } from './utils'; import { sampleDocNoSortId, - sampleSignalAlertParams, + sampleRuleAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, + sampleDocSearchResultsNoSortIdNoVersion, sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, - sampleSignalId, + sampleBulkCreateDuplicateResult, + sampleRuleGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -46,11 +51,12 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { - const sampleParams = sampleSignalAlertParams(undefined); + const fakeUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId, - signalParams: sampleParams, - id: sampleSignalId, + doc: sampleDocNoSortId(fakeUuid), + ruleParams: sampleParams, + id: sampleRuleGuid, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', @@ -59,11 +65,13 @@ describe('utils', () => { }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; + if (fakeSignalSourceHit.signal.parent) { + delete fakeSignalSourceHit.signal.parent.id; + } expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', signal: { parent: { - id: 'someFakeId', type: 'event', index: 'myFakeSignalIndex', depth: 1, @@ -88,7 +96,6 @@ describe('utils', () => { severity: 'high', tags: ['some fake tag'], type: 'query', - size: 1000, status: 'open', to: 'now', enabled: true, @@ -99,9 +106,115 @@ describe('utils', () => { }); }); }); - describe('singleBulkIndex', () => { - test('create successful bulk index', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + describe('singleBulkCreate', () => { + describe('create signal id gereateId', () => { + test('two docs with same index, id, and version should have same id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId); + expect(firstHash).toEqual(generatedHash); + expect(secondHash).toEqual(generatedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + }); + test('two docs with different index, id, and version should have different id', () => { + const findex = 'myfakeindex'; + const findex2 = 'mysecondfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'mysecondfakeindexsomefakeid1rule-1' + const secondGeneratedHash = + 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex2, fid, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, different id, and same version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const fid2 = 'somefakeid2'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'myfakeindexsomefakeid21rule-1' + const secondGeneratedHash = + '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid2, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, same id, and different version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const version2 = '2'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid2rule-1' + const secondGeneratedHash = + 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version2, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { + const longIndexName = + 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const firstHash = generateId(longIndexName, fid, version, ruleId); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + }); + test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const ruleId2 = 'rule-2'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid1rule-2' + const secondGeneratedHash = + '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId2); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + }); + test('create successful bulk create', async () => { + const fakeUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -112,12 +225,12 @@ describe('utils', () => { }, ], }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, - signalParams: sampleParams, + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -125,18 +238,46 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + test('create successful bulk create with docs with no versioning', async () => { + const fakeUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(undefined); + const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create unsuccessful bulk create due to empty search results', async () => { + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); - const successfulSingleBulkIndex = await singleBulkIndex({ + const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -144,22 +285,19 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to bulk index errors', async () => { - // need a sample search result, sample signal params, mock service, mock logger - const sampleParams = sampleSignalAlertParams(undefined); + test('create successful bulk create when bulk create has errors', async () => { + const fakeUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, - }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, - signalParams: sampleParams, + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -168,72 +306,77 @@ describe('utils', () => { enabled: true, }); expect(mockLogger.error).toHaveBeenCalled(); - expect(successfulSingleBulkIndex).toEqual(false); + expect(successfulsingleBulkCreate).toEqual(true); }); }); describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Attempted to search after with empty sort id'); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }); expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Fake Error'); }); }); - describe('searchAfterAndBulkIndex', () => { + describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); - const result = await searchAfterAndBulkIndex({ + const sampleParams = sampleRuleAlertParams(undefined); + const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(30); + const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -244,7 +387,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) .mockReturnValueOnce({ took: 100, errors: false, @@ -254,7 +397,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) .mockReturnValueOnce({ took: 100, errors: false, @@ -264,47 +407,47 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); }); - test('if unsuccessful first bulk index', async () => { - const sampleParams = sampleSignalAlertParams(10); - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, // will cause singleBulkIndex to return false - }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, + test('if unsuccessful first bulk create', async () => { + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + const sampleParams = sampleRuleAlertParams(10); + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids', async () => { - const sampleParams = sampleSignalAlertParams(undefined); - + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { + const sampleParams = sampleRuleAlertParams(undefined); + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -314,24 +457,26 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortId, - signalParams: sampleParams, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(someUuid), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { + const sampleParams = sampleRuleAlertParams(undefined); + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -341,23 +486,26 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortIdNoHits, - signalParams: sampleParams, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); + const oneGuid = uuid.v4(); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -368,24 +516,26 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, + .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -397,53 +547,25 @@ describe('utils', () => { ], }) .mockReturnValueOnce(sampleEmptyDocSearchResults); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleSignalId, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - }); - expect(result).toEqual(true); - }); - test('if logs error when iteration is unsuccessful when bulk index results in a failure', async () => { - const sampleParams = sampleSignalAlertParams(5); - mockService.callCluster - .mockReturnValueOnce({ - // first bulk insert - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(sampleDocSearchResultsWithSortId); // get some more docs - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); - expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(true); }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -454,19 +576,22 @@ describe('utils', () => { }, ], }) - .mockRejectedValueOnce(Error('Fake Error')); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, + .mockImplementation(() => { + throw Error('Fake Error'); + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(false); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 25934dc9aa356a..ba3f310c886ceb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { performance } from 'perf_hooks'; import { pickBy } from 'lodash/fp'; import { SignalHit, Signal } from '../../types'; @@ -12,13 +13,13 @@ import { SignalSourceHit, SignalSearchResponse, BulkResponse, - AlertTypeParams, - OutputSignalES, + RuleTypeParams, + OutputRuleES, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface BuildRuleParams { - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; name: string; id: string; enabled: boolean; @@ -28,47 +29,46 @@ interface BuildRuleParams { } export const buildRule = ({ - signalParams, + ruleParams, name, id, enabled, createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, status: 'open', - rule_id: signalParams.ruleId, - false_positives: signalParams.falsePositives, - saved_id: signalParams.savedId, - meta: signalParams.meta, - max_signals: signalParams.maxSignals, - risk_score: signalParams.riskScore, - output_index: signalParams.outputIndex, - description: signalParams.description, - filter: signalParams.filter, - from: signalParams.from, - immutable: signalParams.immutable, - index: signalParams.index, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + filter: ruleParams.filter, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, interval, - language: signalParams.language, + language: ruleParams.language, name, - query: signalParams.query, - references: signalParams.references, - severity: signalParams.severity, - tags: signalParams.tags, - type: signalParams.type, - size: signalParams.size, - to: signalParams.to, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags: ruleParams.tags, + type: ruleParams.type, + to: ruleParams.to, enabled, - filters: signalParams.filters, + filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { return { parent: { id: doc._id, @@ -83,7 +83,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial) interface BuildBulkBodyParams { doc: SignalSourceHit; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; id: string; name: string; createdBy: string; @@ -95,7 +95,7 @@ interface BuildBulkBodyParams { // format search_after result for signals index. export const buildBulkBody = ({ doc, - signalParams, + ruleParams, id, name, createdBy, @@ -104,7 +104,7 @@ export const buildBulkBody = ({ enabled, }: BuildBulkBodyParams): SignalHit => { const rule = buildRule({ - signalParams, + ruleParams, id, name, enabled, @@ -121,9 +121,9 @@ export const buildBulkBody = ({ return signalHit; }; -interface SingleBulkIndexParams { +interface SingleBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -135,10 +135,20 @@ interface SingleBulkIndexParams { enabled: boolean; } +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); + // Bulk Index documents. -export const singleBulkIndex = async ({ +export const singleBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -148,18 +158,32 @@ export const singleBulkIndex = async ({ updatedBy, interval, enabled, -}: SingleBulkIndexParams): Promise => { +}: SingleBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } + // index documents after creating an ID based on the + // source documents' originating index, and the original + // document _id. This will allow two documents from two + // different indexes with the same ID to be + // indexed, and prevents us from creating any updates + // to the documents once inserted into the signals index, + // while preventing duplicates from being added to the + // signals index if rules are re-run over the same time + // span. Also allow for versioning. const bulkBody = someResult.hits.hits.flatMap(doc => [ { - index: { + create: { _index: signalsIndex, - _id: doc._id, + _id: generateId( + doc._index, + doc._id, + doc._version ? doc._version.toString() : '', + ruleParams.ruleId ?? '' + ), }, }, - buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled }), ]); const time1 = performance.now(); const firstResult: BulkResponse = await services.callCluster('bulk', { @@ -171,36 +195,57 @@ export const singleBulkIndex = async ({ logger.debug(`individual bulk process time took: ${time2 - time1} milliseconds`); logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); if (firstResult.errors) { - logger.error(`[-] bulkResponse had errors: ${JSON.stringify(firstResult.errors, null, 2)}`); - return false; + // go through the response status errors and see what + // types of errors they are, count them up, and log them. + const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { + if (item.create.error) { + const responseStatusKey = item.create.status.toString(); + acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; + } + return acc; + }, {}); + /* + the logging output below should look like + {'409': 55} + which is read as "there were 55 counts of 409 errors returned from bulk create" + */ + logger.error( + `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( + errorCountMap, + null, + 2 + )}` + ); } return true; }; interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; + pageSize: number; } // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - signalParams, + ruleParams, services, logger, + pageSize, }: SingleSearchAfterParams): Promise => { if (searchAfterSortId == null) { throw Error('Attempted to search after with empty sort id'); } try { const searchAfterQuery = buildEventsSearchQuery({ - index: signalParams.index, - from: signalParams.from, - to: signalParams.to, - filter: signalParams.filter, - size: signalParams.size ? signalParams.size : 1000, + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter: ruleParams.filter, + size: pageSize, searchAfterSortId, }); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( @@ -214,9 +259,9 @@ export const singleSearchAfter = async ({ } }; -interface SearchAfterAndBulkIndexParams { +interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -226,12 +271,13 @@ interface SearchAfterAndBulkIndexParams { updatedBy: string; interval: string; enabled: boolean; + pageSize: number; } // search_after through documents and re-index using bulk endpoint. -export const searchAfterAndBulkIndex = async ({ +export const searchAfterAndBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -241,15 +287,16 @@ export const searchAfterAndBulkIndex = async ({ updatedBy, interval, enabled, -}: SearchAfterAndBulkIndexParams): Promise => { + pageSize, +}: SearchAfterAndBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } logger.debug('[+] starting bulk insertion'); - const firstBulkIndexSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -260,18 +307,14 @@ export const searchAfterAndBulkIndex = async ({ interval, enabled, }); - if (!firstBulkIndexSuccess) { - logger.error('First bulk index was unsuccessful'); - return false; - } - const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; // maxTotalHitsSize represents the total number of docs to - // query for. If maxSignals is present we will only query - // up to max signals - otherwise use the value - // from track_total_hits. - const maxTotalHitsSize = signalParams.maxSignals ? signalParams.maxSignals : totalHits; + // query for, no matter the size of each individual page of search results. + // If the total number of hits for the overall search result is greater than + // maxSignals, default to requesting a total of maxSignals, otherwise use the + // totalHits in the response from the searchAfter query. + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -292,9 +335,10 @@ export const searchAfterAndBulkIndex = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - signalParams, + ruleParams, services, logger, + pageSize, // maximum number of docs to receive per search result. }); if (searchAfterResult.hits.hits.length === 0) { return true; @@ -308,9 +352,9 @@ export const searchAfterAndBulkIndex = async ({ } sortId = sortIds[0]; logger.debug('next bulk index'); - const bulkSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult: searchAfterResult, - signalParams, + ruleParams, services, logger, id, @@ -322,14 +366,11 @@ export const searchAfterAndBulkIndex = async ({ enabled, }); logger.debug('finished next bulk index'); - if (!bulkSuccess) { - logger.error('[-] bulk index failed but continuing'); - } } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); return false; } } - logger.debug(`[+] completed bulk index of ${totalHits}`); + logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); return true; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c74d2e87a7ef6b..4c49326fbb32a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,13 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. -export const typicalPayload = (): Partial> => ({ +export const typicalPayload = (): Partial> => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -28,7 +28,7 @@ export const typicalPayload = (): Partial> language: 'kuery', }); -export const typicalFilterPayload = (): Partial => ({ +export const typicalFilterPayload = (): Partial => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -64,7 +64,7 @@ interface FindHit { page: number; perPage: number; total: number; - data: SignalAlertType[]; + data: RuleAlertType[]; } export const getFindResult = (): FindHit => ({ @@ -81,7 +81,7 @@ export const getFindResultWithSingleHit = (): FindHit => ({ data: [getResult()], }); -export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ +export const getFindResultWithMultiHits = (data: RuleAlertType[]): FindHit => ({ page: 1, perPage: 1, total: 2, @@ -113,7 +113,7 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const getResult = (): SignalAlertType => ({ +export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 1232fe3ce219d3..4c222c196300ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { createSignalsRoute } from './create_signals_route'; +import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -21,17 +21,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('create_signals', () => { +describe('create_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - createSignalsRoute(server); + createRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); @@ -42,14 +42,14 @@ describe('create_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createSignalsRoute(serverWithoutActionClient); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createSignalsRoute(serverWithoutAlertClient); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); @@ -58,7 +58,7 @@ describe('create_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createSignalsRoute(serverWithoutActionOrAlertClient); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts similarity index 71% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index 7b6559561c7833..7e1ac07e1f0aaa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -9,14 +9,14 @@ import { isFunction } from 'lodash/fp'; import Boom from 'boom'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createSignals } from '../alerts/create_signals'; -import { SignalsRequest } from '../alerts/types'; -import { createSignalsSchema } from './schemas'; +import { createRules } from '../alerts/create_rules'; +import { RulesRequest } from '../alerts/types'; +import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { transformOrError } from './utils'; -export const createCreateSignalsRoute: Hapi.ServerRoute = { +export const createCreateRulesRoute: Hapi.ServerRoute = { method: 'POST', path: DETECTION_ENGINE_RULES_URL, options: { @@ -25,10 +25,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: createSignalsSchema, + payload: createRulesSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: RulesRequest, headers) { const { description, enabled, @@ -49,7 +49,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -64,13 +63,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { } if (ruleId != null) { - const signal = await readSignals({ alertsClient, ruleId }); - if (signal != null) { - return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); } } - const createdSignal = await createSignals({ + const createdRule = await createRules({ alertsClient, actionsClient, description, @@ -92,16 +91,15 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, references, }); - return transformOrError(createdSignal); + return transformOrError(createdRule); }, }; -export const createSignalsRoute = (server: ServerFacade) => { - server.route(createCreateSignalsRoute); +export const createRulesRoute = (server: ServerFacade) => { + server.route(createCreateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts index 95816aa55d1fea..0808051964dc1d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { deleteSignalsRoute } from './delete_signals_route'; +import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -22,12 +22,12 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('delete_signals', () => { +describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient } = createMockServer()); - deleteSignalsRoute(server); + deleteRulesRoute(server); }); afterEach(() => { @@ -35,7 +35,7 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -43,7 +43,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -51,7 +51,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { + test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -61,14 +61,14 @@ describe('delete_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteSignalsRoute(serverWithoutActionClient); + deleteRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteSignalsRoute(serverWithoutAlertClient); + deleteRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); @@ -77,7 +77,7 @@ describe('delete_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteSignalsRoute(serverWithoutActionOrAlertClient); + deleteRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts index 1f5494a54ddca8..12dff0dd60c147 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -8,13 +8,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteSignals } from '../alerts/delete_signals'; +import { deleteRules } from '../alerts/delete_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; import { getIdError, transformOrError } from './utils'; -export const createDeleteSignalsRoute: Hapi.ServerRoute = { +export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -35,21 +35,21 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await deleteSignals({ + const rule = await deleteRules({ actionsClient, alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const deleteSignalsRoute = (server: ServerFacade): void => { - server.route(createDeleteSignalsRoute); +export const deleteRulesRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts index be3dce36e87167..dae40f05155dc5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts @@ -11,17 +11,17 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { findSignalsRoute } from './find_signals_route'; +import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient, actionsClient } = createMockServer()); - findSignalsRoute(server); + findRulesRoute(server); }); afterEach(() => { @@ -29,7 +29,7 @@ describe('find_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); actionsClient.find.mockResolvedValue({ page: 1, @@ -44,14 +44,14 @@ describe('find_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findSignalsRoute(serverWithoutActionClient); + findRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findSignalsRoute(serverWithoutAlertClient); + findRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); @@ -60,7 +60,7 @@ describe('find_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - findSignalsRoute(serverWithoutActionOrAlertClient); + findRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts similarity index 70% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts index 120b71fab7d3ae..893fb3f689d164 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findSignals } from '../alerts/find_signals'; -import { FindSignalsRequest } from '../alerts/types'; -import { findSignalsSchema } from './schemas'; +import { findRules } from '../alerts/find_rules'; +import { FindRulesRequest } from '../alerts/types'; +import { findRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { transformFindAlertsOrError } from './utils'; -export const createFindSignalRoute: Hapi.ServerRoute = { +export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { @@ -22,10 +22,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: findSignalsSchema, + query: findRulesSchema, }, }, - async handler(request: FindSignalsRequest, headers) { + async handler(request: FindRulesRequest, headers) { const { query } = request; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -34,7 +34,7 @@ export const createFindSignalRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signals = await findSignals({ + const rules = await findRules({ alertsClient, perPage: query.per_page, page: query.page, @@ -42,10 +42,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { sortOrder: query.sort_order, filter: query.filter, }); - return transformFindAlertsOrError(signals); + return transformFindAlertsOrError(rules); }, }; -export const findSignalsRoute = (server: ServerFacade) => { - server.route(createFindSignalRoute); +export const findRulesRoute = (server: ServerFacade) => { + server.route(createFindRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts index 021bcc7b8b48e0..47ecf62f41be9b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { readSignalsRoute } from './read_signals_route'; +import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -26,7 +26,7 @@ describe('read_signals', () => { beforeEach(() => { ({ server, alertsClient } = createMockServer()); - readSignalsRoute(server); + readRulesRoute(server); }); afterEach(() => { @@ -34,7 +34,7 @@ describe('read_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); @@ -43,14 +43,14 @@ describe('read_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readSignalsRoute(serverWithoutActionClient); + readRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readSignalsRoute(serverWithoutAlertClient); + readRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); @@ -59,7 +59,7 @@ describe('read_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - readSignalsRoute(serverWithoutActionOrAlertClient); + readRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts index 2d662f9049cce2..4642c34fbe3393 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -9,12 +9,12 @@ import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { getIdError, transformOrError } from './utils'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; -export const createReadSignalsRoute: Hapi.ServerRoute = { +export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -34,19 +34,19 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - const signal = await readSignals({ + const rule = await readRules({ alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const readSignalsRoute = (server: ServerFacade) => { - server.route(createReadSignalsRoute); +export const readRulesRoute = (server: ServerFacade) => { + server.route(createReadRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 5e5f37ca8a0802..6c7e5c4054326d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createSignalsSchema, - updateSignalSchema, - findSignalsSchema, - querySignalSchema, -} from './schemas'; -import { - SignalAlertParamsRest, - FindParamsRest, - UpdateSignalAlertParamsRest, -} from '../alerts/types'; +import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; describe('schemas', () => { - describe('create signals schema', () => { + describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -32,7 +23,7 @@ describe('schemas', () => { test('[rule_id] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -40,7 +31,7 @@ describe('schemas', () => { test('[rule_id, description] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -49,7 +40,7 @@ describe('schemas', () => { test('[rule_id, description, from] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -59,7 +50,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -70,7 +61,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -82,7 +73,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +86,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -109,7 +100,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -124,7 +115,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -140,7 +131,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -158,7 +149,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -174,9 +165,9 @@ describe('schemas', () => { ).toBeTruthy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -190,12 +181,12 @@ describe('schemas', () => { query: 'some query', language: 'kuery', }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -213,9 +204,9 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -228,12 +219,12 @@ describe('schemas', () => { filter: {}, risk_score: 50, }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -252,7 +243,7 @@ describe('schemas', () => { test('If filter type is set then filter is required', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -270,7 +261,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -290,7 +281,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -310,7 +301,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -330,7 +321,7 @@ describe('schemas', () => { test('allows references to be sent as valid', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -351,7 +342,7 @@ describe('schemas', () => { test('defaults references to an array', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -371,8 +362,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { references: number[] } + createRulesSchema.validate< + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,8 +385,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { index: number[] } + createRulesSchema.validate< + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -416,7 +407,7 @@ describe('schemas', () => { test('defaults interval to 5 min', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -433,7 +424,7 @@ describe('schemas', () => { test('defaults max signals to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -451,7 +442,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -471,7 +462,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -489,7 +480,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -508,7 +499,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -528,8 +519,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - createSignalsSchema.validate< - Partial & { filters: string }> + createRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -550,7 +541,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -570,7 +561,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -591,7 +582,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -612,7 +603,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -633,7 +624,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -655,7 +646,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -677,7 +668,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -699,7 +690,7 @@ describe('schemas', () => { test('You can optionally send in an array of tags', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -722,32 +713,32 @@ describe('schemas', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { tags: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error + createRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error ).toBeTruthy(); }); test('You can optionally send in an array of false positives', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -770,8 +761,8 @@ describe('schemas', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { false_positives: number[] } + createRulesSchema.validate< + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -795,7 +786,7 @@ describe('schemas', () => { test('You can optionally set the immutable to be true', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -818,8 +809,8 @@ describe('schemas', () => { test('You cannot set the immutable to be a number', () => { expect( - createSignalsSchema.validate< - Partial> & { immutable: number } + createRulesSchema.validate< + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -843,7 +834,7 @@ describe('schemas', () => { test('You cannot set the risk_score to 101', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -866,7 +857,7 @@ describe('schemas', () => { test('You cannot set the risk_score to -1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -889,7 +880,7 @@ describe('schemas', () => { test('You can set the risk_score to 0', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -912,7 +903,7 @@ describe('schemas', () => { test('You can set the risk_score to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -935,7 +926,7 @@ describe('schemas', () => { test('You can set meta to any object you want', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -961,9 +952,7 @@ describe('schemas', () => { test('You cannot create meta as a string', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -987,9 +976,7 @@ describe('schemas', () => { test('You can have an empty query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1013,9 +1000,7 @@ describe('schemas', () => { test('You can omit the query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1038,9 +1023,7 @@ describe('schemas', () => { test('query string defaults to empty string when present with filters', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1062,16 +1045,14 @@ describe('schemas', () => { }); }); - describe('update signals schema', () => { + describe('update rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect( - updateSignalSchema.validate>({}).error - ).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1079,7 +1060,7 @@ describe('schemas', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); @@ -1087,7 +1068,7 @@ describe('schemas', () => { test('[rule_id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeFalsy(); @@ -1095,7 +1076,7 @@ describe('schemas', () => { test('[id and rule_id] does not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'id-1', rule_id: 'rule-1', }).error @@ -1104,7 +1085,7 @@ describe('schemas', () => { test('[rule_id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -1113,7 +1094,7 @@ describe('schemas', () => { test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', }).error @@ -1122,7 +1103,7 @@ describe('schemas', () => { test('[id, risk_score] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', risk_score: 10, }).error @@ -1131,7 +1112,7 @@ describe('schemas', () => { test('[rule_id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1141,7 +1122,7 @@ describe('schemas', () => { test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1151,7 +1132,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1162,7 +1143,7 @@ describe('schemas', () => { test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1173,7 +1154,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1185,7 +1166,7 @@ describe('schemas', () => { test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1197,7 +1178,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1210,7 +1191,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1223,7 +1204,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1237,7 +1218,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1251,7 +1232,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1266,7 +1247,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1281,7 +1262,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1297,7 +1278,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1313,7 +1294,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1330,7 +1311,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1347,7 +1328,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1365,7 +1346,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1383,7 +1364,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1400,7 +1381,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1417,7 +1398,7 @@ describe('schemas', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1433,7 +1414,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1451,7 +1432,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1469,7 +1450,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1487,7 +1468,7 @@ describe('schemas', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1506,7 +1487,7 @@ describe('schemas', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1524,7 +1505,7 @@ describe('schemas', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1539,7 +1520,7 @@ describe('schemas', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1555,8 +1536,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { references: number[] } + updateRulesSchema.validate< + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1576,8 +1557,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { index: number[] } + updateRulesSchema.validate< + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1596,7 +1577,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1614,7 +1595,7 @@ describe('schemas', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1630,7 +1611,7 @@ describe('schemas', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1647,7 +1628,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1665,7 +1646,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1683,7 +1664,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1702,7 +1683,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1721,7 +1702,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1740,7 +1721,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1760,7 +1741,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1780,7 +1761,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1800,7 +1781,7 @@ describe('schemas', () => { test('meta can be updated', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', meta: { whateverYouWant: 'anything_at_all' }, }).error @@ -1809,8 +1790,8 @@ describe('schemas', () => { test('You update meta as a string', () => { expect( - updateSignalSchema.validate< - Partial & { meta: string }> + updateRulesSchema.validate< + Partial & { meta: string }> >({ id: 'rule-1', meta: 'should not work', @@ -1820,8 +1801,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - updateSignalSchema.validate< - Partial & { filters: string }> + updateRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', type: 'query', @@ -1831,14 +1812,14 @@ describe('schemas', () => { }); }); - describe('find signals schema', () => { + describe('find rules schema', () => { test('empty objects do validate', () => { - expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); test('all values validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ per_page: 5, page: 1, sort_field: 'some field', @@ -1851,7 +1832,7 @@ describe('schemas', () => { test('made up parameters do not validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1859,31 +1840,31 @@ describe('schemas', () => { test('per_page validates', () => { expect( - findSignalsSchema.validate>({ per_page: 5 }).error + findRulesSchema.validate>({ per_page: 5 }).error ).toBeFalsy(); }); test('page validates', () => { expect( - findSignalsSchema.validate>({ page: 5 }).error + findRulesSchema.validate>({ page: 5 }).error ).toBeFalsy(); }); test('sort_field validates', () => { expect( - findSignalsSchema.validate>({ sort_field: 'some value' }).error + findRulesSchema.validate>({ sort_field: 'some value' }).error ).toBeFalsy(); }); test('fields validates with a string', () => { expect( - findSignalsSchema.validate>({ fields: ['some value'] }).error + findRulesSchema.validate>({ fields: ['some value'] }).error ).toBeFalsy(); }); test('fields validates with multiple strings', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ fields: ['some value 1', 'some value 2'], }).error ).toBeFalsy(); @@ -1891,23 +1872,23 @@ describe('schemas', () => { test('fields does not validate with a number', () => { expect( - findSignalsSchema.validate> & { fields: number[] }>({ + findRulesSchema.validate> & { fields: number[] }>({ fields: [5], }).error ).toBeTruthy(); }); test('per page has a default of 20', () => { - expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); }); test('page has a default of 1', () => { - expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + expect(findRulesSchema.validate>({}).value.page).toEqual(1); }); test('filter works with a string', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ filter: 'some value 1', }).error ).toBeFalsy(); @@ -1915,7 +1896,7 @@ describe('schemas', () => { test('filter does not work with a number', () => { expect( - findSignalsSchema.validate> & { filter: number }>({ + findRulesSchema.validate> & { filter: number }>({ filter: 5, }).error ).toBeTruthy(); @@ -1923,7 +1904,7 @@ describe('schemas', () => { test('sort_order requires sort_field to work', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', }).error ).toBeTruthy(); @@ -1931,7 +1912,7 @@ describe('schemas', () => { test('sort_order and sort_field validate together', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', sort_field: 'some field', }).error @@ -1940,7 +1921,7 @@ describe('schemas', () => { test('sort_order validates with desc and sort_field', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'desc', sort_field: 'some field', }).error @@ -1949,7 +1930,7 @@ describe('schemas', () => { test('sort_order does not validate with a string other than asc and desc', () => { expect( - findSignalsSchema.validate< + findRulesSchema.validate< Partial> & { sort_order: string } >({ sort_order: 'some other string', @@ -1959,29 +1940,27 @@ describe('schemas', () => { }); }); - describe('querySignalSchema', () => { + describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect( - querySignalSchema.validate>({}).error - ).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied dot not validate', () => { expect( - querySignalSchema.validate>({ rule_id: '1', id: '1' }) + queryRulesSchema.validate>({ rule_id: '1', id: '1' }) .error ).toBeTruthy(); }); test('only id validates', () => { expect( - querySignalSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - querySignalSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index fa773b684eb5db..664a98ad7d7ddd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; /* eslint-disable @typescript-eslint/camelcase */ const description = Joi.string(); @@ -51,7 +52,7 @@ const fields = Joi.array() .single(); /* eslint-enable @typescript-eslint/camelcase */ -export const createSignalsSchema = Joi.object({ +export const createRulesSchema = Joi.object({ description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -68,7 +69,7 @@ export const createSignalsSchema = Joi.object({ from: from.required(), rule_id, immutable: immutable.default(false), - index: index.required(), + index, interval: interval.default('5m'), query: Joi.when('type', { is: 'query', @@ -95,7 +96,7 @@ export const createSignalsSchema = Joi.object({ otherwise: Joi.forbidden(), }), }), - output_index: output_index.required(), + output_index, saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), @@ -103,7 +104,7 @@ export const createSignalsSchema = Joi.object({ }), meta, risk_score: risk_score.required(), - max_signals: max_signals.default(100), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), severity: severity.required(), tags: tags.default([]), @@ -112,7 +113,7 @@ export const createSignalsSchema = Joi.object({ references: references.default([]), }); -export const updateSignalSchema = Joi.object({ +export const updateRulesSchema = Joi.object({ description, enabled, false_positives, @@ -167,12 +168,12 @@ export const updateSignalSchema = Joi.object({ references, }).xor('id', 'rule_id'); -export const querySignalSchema = Joi.object({ +export const queryRulesSchema = Joi.object({ rule_id, id, }).xor('id', 'rule_id'); -export const findSignalsSchema = Joi.object({ +export const findRulesSchema = Joi.object({ fields, filter: queryFilter, per_page, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts index 7288d18628316b..d03d68417dd5d5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { updateSignalsRoute } from './update_signals_route'; +import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -24,17 +24,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('update_signals', () => { +describe('update_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - updateSignalsRoute(server); + updateRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -43,7 +43,7 @@ describe('update_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when updating a single signal that does not exist', async () => { + test('returns 404 when updating a single rule that does not exist', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -54,14 +54,14 @@ describe('update_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateSignalsRoute(serverWithoutActionClient); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateSignalsRoute(serverWithoutAlertClient); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); @@ -70,7 +70,7 @@ describe('update_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateSignalsRoute(serverWithoutActionOrAlertClient); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index 274c41f65a36be..1cc65054527c09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateSignal } from '../alerts/update_signals'; -import { UpdateSignalsRequest } from '../alerts/types'; -import { updateSignalSchema } from './schemas'; +import { updateRules } from '../alerts/update_rules'; +import { UpdateRulesRequest } from '../alerts/types'; +import { updateRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { getIdError, transformOrError } from './utils'; -export const createUpdateSignalsRoute: Hapi.ServerRoute = { +export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, options: { @@ -22,10 +22,10 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: updateSignalSchema, + payload: updateRulesSchema, }, }, - async handler(request: UpdateSignalsRequest, headers) { + async handler(request: UpdateRulesRequest, headers) { const { description, enabled, @@ -47,7 +47,6 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -61,7 +60,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await updateSignal({ + const rule = await updateRules({ alertsClient, actionsClient, description, @@ -84,20 +83,19 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, references, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const updateSignalsRoute = (server: ServerFacade) => { - server.route(createUpdateSignalsRoute); +export const updateRulesRoute = (server: ServerFacade) => { + server.route(createUpdateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 22dd7be5fbba71..632778d78dab7d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { - transformAlertToSignal, + transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, @@ -14,11 +14,11 @@ import { import { getResult } from './__mocks__/request_responses'; describe('utils', () => { - describe('transformAlertToSignal', () => { + describe('transformAlertToRule', () => { test('should work with a full data set', () => { - const fullSignal = getResult(); - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -37,7 +37,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -46,8 +45,8 @@ describe('utils', () => { }); test('should work with a partial data set missing data', () => { - const fullSignal = getResult(); - const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -65,7 +64,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -74,10 +72,10 @@ describe('utils', () => { }); test('should omit query if query is null', () => { - const fullSignal = getResult(); - fullSignal.params.query = null; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -95,7 +93,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -104,10 +101,10 @@ describe('utils', () => { }); test('should omit query if query is undefined', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -125,7 +122,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -134,10 +130,10 @@ describe('utils', () => { }); test('should omit a mix of undefined, null, and missing fields', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - fullSignal.params.language = null; - const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -153,7 +149,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -162,10 +157,10 @@ describe('utils', () => { }); test('should return enabled is equal to false', () => { - const fullSignal = getResult(); - fullSignal.enabled = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -184,7 +179,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -193,10 +187,10 @@ describe('utils', () => { }); test('should return immutable is equal to false', () => { - const fullSignal = getResult(); - fullSignal.params.immutable = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -215,7 +209,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -297,7 +290,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -335,7 +327,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index e3a677741efca2..eb0ae49436bcaf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; -import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; +import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; export const getIdError = ({ id, @@ -26,50 +26,49 @@ export const getIdError = ({ // Transforms the data but will remove any null or undefined it encounters and not include // those on the export -export const transformAlertToSignal = (signal: SignalAlertType): Partial => { - return pickBy((value: unknown) => value != null, { - created_by: signal.createdBy, - description: signal.params.description, - enabled: signal.enabled, - false_positives: signal.params.falsePositives, - filter: signal.params.filter, - filters: signal.params.filters, - from: signal.params.from, - id: signal.id, - immutable: signal.params.immutable, - index: signal.params.index, - interval: signal.interval, - rule_id: signal.params.ruleId, - language: signal.params.language, - output_index: signal.params.outputIndex, - max_signals: signal.params.maxSignals, - risk_score: signal.params.riskScore, - name: signal.name, - query: signal.params.query, - references: signal.params.references, - saved_id: signal.params.savedId, - meta: signal.params.meta, - severity: signal.params.severity, - size: signal.params.size, - updated_by: signal.updatedBy, - tags: signal.params.tags, - to: signal.params.to, - type: signal.params.type, +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filter: alert.params.filter, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.params.tags, + to: alert.params.to, + type: alert.params.type, }); }; export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; } else { return new Boom('Internal error transforming', { statusCode: 500 }); } }; -export const transformOrError = (signal: unknown): Partial | Boom => { - if (isAlertType(signal)) { - return transformAlertToSignal(signal); +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); } else { return new Boom('Internal error transforming', { statusCode: 500 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md index b3ab0011e1f8fc..8d617a8de3fcde 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md @@ -4,6 +4,7 @@ search which is not available in the DEV console for the detection engine. Before beginning ensure in your .zshrc/.bashrc you have your user, password, and url set: Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -21,6 +22,7 @@ And that you have the latest version of [NodeJS](https://nodejs.org/en/), [CURL](https://curl.haxx.se), and [jq](https://stedolan.github.io/jq/) installed. If you have homebrew you can install using brew like so + ``` brew install jq ``` @@ -29,10 +31,9 @@ After that you can execute scripts within this folder by first ensuring your current working directory is `./scripts` and then running any scripts within that folder. -Example to add a signal to the system +Example to add a rule to the system ``` cd ./scripts -./post_signal.sh ./signals/root_or_admin_1.json +./post_rule.sh ./rules/root_or_admin_1.json ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh index 802273c67849df..e4d345eec0b656 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh @@ -9,4 +9,4 @@ set -e ./check_env_variables.sh -node ../../../../scripts/convert_saved_search_to_signals.js $1 $2 +node ../../../../scripts/convert_saved_search_to_rules.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh index 25cd4bfd336284..2db5740c79bb84 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_id.sh ${id} +# Example: ./delete_rule_by_id.sh ${id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh index b74ee260ad8adb..80ef849828b781 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_rule_id.sh ${rule_id} +# Example: ./delete_rule_by_rule_id.sh ${rule_id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh index 34c3c401b41120..34b6208947c573 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -11,8 +11,8 @@ set -e FILTER=${1:-'alert.attributes.enabled:%20true'} -# Example: ./find_signal_by_filter.sh "alert.attributes.enabled:%20true" -# Example: ./find_signal_by_filter.sh "alert.attributes.name:%20Detect*" +# Example: ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" +# Example: ./find_rule_by_filter.sh "alert.attributes.name:%20Detect*" # The %20 is just an encoded space that is typical of URL's. # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh index 4542eb7c9a8273..520b4afa24cd2f 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./find_signals.sh +# Example: ./find_rules.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh index 122f18bbb80e5b..8e6690d848db42 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh @@ -12,7 +12,7 @@ set -e SORT=${1:-'enabled'} ORDER=${2:-'asc'} -# Example: ./find_signals_sort.sh enabled asc +# Example: ./find_rules_sort.sh enabled asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh index 239a04846b11ab..dba5652390ea97 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_id.sh {rule_id} +# Example: ./get_rule_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh index 5100caac32491b..114b6570a03e22 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_rule_id.sh {rule_id} +# Example: ./get_rule_by_rule_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh similarity index 64% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh index 8455e7d27ad472..591cf7625e2e3b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh @@ -10,21 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_1.json}) +RULES=(${@:-./rules/root_or_admin_1.json}) -# Example: ./post_signal.sh -# Example: ./post_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./post_rule.sh +# Example: ./post_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index 8362c576ff554c..53e7bb504746d9 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -12,8 +12,8 @@ set -e # Uses a default of 100 if no argument is specified NUMBER=${1:-100} -# Example: ./post_x_signals.sh -# Example: ./post_x_signals.sh 200 +# Example: ./post_x_rules.sh +# Example: ./post_x_rules.sh 200 for i in $(seq 1 $NUMBER); do { curl -s -k \ @@ -24,7 +24,6 @@ do { --data "{ \"rule_id\": \"${i}\", \"risk_score\": \"50\", - \"output_index\": \"${SIGNALS_INDEX}"\", \"description\": \"Detecting root and admin users\", \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], \"interval\": \"24h\", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json index 8586b29c29886a..b00a5929d9ef1a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json @@ -2,10 +2,8 @@ "rule_id": "rule-1", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json index 85bc09f0f9f850..657439104e3064 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json @@ -1,13 +1,11 @@ { "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "references": ["http://www.example.com", "https://ww.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json similarity index 76% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json index 8f2d826ae9ae19..137cf7eedbccf1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json @@ -2,9 +2,7 @@ "rule_id": "rule-2", "risk_score": 2, "description": "Detecting root and admin users over a long period of time", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", - "output_index": ".siem-signals", "name": "Detect Root/Admin Users over a long period of time", "severity": "high", "type": "query", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json similarity index 74% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json index 10bfc2e0d74a36..b9160c95621ee8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json @@ -2,10 +2,8 @@ "rule_id": "rule-3", "risk_score": 3, "description": "Detecting root and admin users as an empty set", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-16y", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json index 18cfb808007b33..364e7f00c95714 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json @@ -2,10 +2,8 @@ "rule_id": "rule-4", "risk_score": 4, "description": "Detecting root and admin users with lucene", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json index a445a839a82289..eb7f2ae03b64b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json @@ -2,10 +2,8 @@ "rule_id": "rule-5", "risk_score": 5, "description": "Detecting root and admin users over 24 hours on windows", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json index 6e2f7a3f82a50f..94f30bc9f92df2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json @@ -2,10 +2,8 @@ "rule_id": "rule-6", "risk_score": 6, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json index 9da8a11861a4de..81ec19a4fd0ef0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json @@ -2,14 +2,12 @@ "rule_id": "rule-7", "risk_score": 7, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "lucene", "filters": [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json index ad8c651bb3ec8a..de24263c6af5cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json @@ -2,14 +2,12 @@ "rule_id": "rule-8", "risk_score": 8, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json index 3658e6e4e94287..9bf2b1abf5f909 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json @@ -2,14 +2,12 @@ "rule_id": "rule-9", "risk_score": 9, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json index db53ea07fe34b0..2381e9e259c07d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json index e6cc661af404cb..ee8fe1fc93fb36 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json index 266ceeba15d479..ed8f2e5745bea1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json @@ -2,10 +2,8 @@ "rule_id": "rule-meta-data", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json similarity index 71% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json index d5559ebe23bdb6..721644acd989df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-1", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json similarity index 76% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json index e272273d817d29..b733b6bb8c592f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-2", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json similarity index 71% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json index 9fc2c32c7daf11..df1b37f19bf291 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-3", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json similarity index 85% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json index 42834141a72fda..09ddfb1c34a924 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json @@ -2,12 +2,10 @@ "rule_id": "rule-1", "risk_score": 98, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-6m", "to": "now-5m", "query": "user.name: root", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json index 4c03f041e6e2f5..8a3c765519ef36 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json @@ -2,7 +2,6 @@ "rule_id": "rule-1", "risk_score": 78, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", @@ -12,7 +11,6 @@ "immutable": true, "tags": ["some other tag for you"], "to": "now-5m", - "output_index": ".siem-signals", "query": "user.name: root", "language": "kuery", "references": ["https://update1.example.com", "https://update2.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json similarity index 73% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json index cfb5fab8b84931..a43398bd6876a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json @@ -2,12 +2,10 @@ "rule_id": "rule-longmont", "risk_score": 5, "description": "Detect Longmont activity", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Longmont activity", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-1y", "to": "now", "query": "user.name: root or user.name: admin", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh similarity index 63% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index 6984e7b4c810b0..8e1abc70456020 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -10,21 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) +RULES=(${@:-./rules/root_or_admin_update_1.json}) -# Example: ./update_signal.sh -# Example: ./update_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./update_rule.sh +# Example: ./update_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 13d040b969545a..9c0059d0d109db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { SignalAlertParamsRest } from './detection_engine/alerts/types'; +import { RuleAlertParamsRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index 2d85a61b048523..bc48d6d6312fb7 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -18,6 +18,8 @@ jest.mock('ui/i18n', () => { return { I18nContext }; }); +jest.mock('ui/new_platform'); + const POLICY_NAME = 'my_policy'; const SNAPSHOT_NAME = 'my_snapshot'; const MIN_COUNT = '5'; @@ -141,6 +143,25 @@ describe.skip('', () => { 'Minimum count cannot be greater than maximum count.', ]); }); + + test('should not allow negative values for the delete after, minimum and maximum counts', () => { + const { find, form } = testBed; + + form.setInputValue('expireAfterValueInput', '-1'); + find('expireAfterValueInput').simulate('blur'); + + form.setInputValue('minCountInput', '-1'); + find('minCountInput').simulate('blur'); + + form.setInputValue('maxCountInput', '-1'); + find('maxCountInput').simulate('blur'); + + expect(form.getErrorsMessages()).toEqual([ + 'Delete after cannot be negative.', + 'Minimum count cannot be negative.', + 'Maximum count cannot be negative.', + ]); + }); }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx index c88cbd2736df6d..df7e2c8807d9f1 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx @@ -85,7 +85,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ } describedByIds={['expirationDescription']} isInvalid={touched.expireAfterValue && Boolean(errors.expireAfterValue)} - error={errors.expireAfter} + error={errors.expireAfterValue} fullWidth > @@ -100,6 +100,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="expireAfterValueInput" + min={0} /> @@ -167,6 +168,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="minCountInput" + min={0} /> @@ -179,6 +181,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ /> } describedByIds={['countDescription']} + isInvalid={touched.maxCount && Boolean(errors.maxCount)} error={errors.maxCount} fullWidth > @@ -193,6 +196,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="maxCountInput" + min={0} /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts index 80734d2f0522c7..3f27da82bf56d1 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts @@ -28,7 +28,9 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { schedule: [], repository: [], indices: [], + expireAfterValue: [], minCount: [], + maxCount: [], }, }; @@ -92,6 +94,34 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { }) ); } + + if (retention && retention.expireAfterValue && retention.expireAfterValue < 0) { + validation.errors.expireAfterValue.push( + i18n.translate( + 'xpack.snapshotRestore.policyValidation.invalidNegativeDeleteAfterErrorMessage', + { + defaultMessage: 'Delete after cannot be negative.', + } + ) + ); + } + + if (retention && retention.minCount && retention.minCount < 0) { + validation.errors.minCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMinCountErrorMessage', { + defaultMessage: 'Minimum count cannot be negative.', + }) + ); + } + + if (retention && retention.maxCount && retention.maxCount < 0) { + validation.errors.maxCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMaxCountErrorMessage', { + defaultMessage: 'Maximum count cannot be negative.', + }) + ); + } + // Remove fields with no errors validation.errors = Object.entries(validation.errors) .filter(([key, value]) => value.length > 0) diff --git a/x-pack/package.json b/x-pack/package.json index f84db22fe5c40e..eccc5918e6d506 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -14,8 +14,7 @@ "test:browser:dev": "gulp testbrowser-dev", "test:browser": "gulp testbrowser", "test:jest": "node scripts/jest", - "test:mocha": "node scripts/mocha", - "test:server": "gulp testserver" + "test:mocha": "node scripts/mocha" }, "kibana": { "build": { @@ -133,7 +132,6 @@ "graphql-codegen-typescript-resolvers": "^0.18.2", "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", - "gulp-mocha": "^7.0.2", "hapi": "^17.5.3", "jest": "^24.9.0", "jest-cli": "^24.9.0", @@ -212,6 +210,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index e2d1892b1355e0..cc4a7c90de513d 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_act import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction } from './custom_time_range_action'; @@ -24,12 +24,12 @@ import { CustomTimeRangeBadge } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; } diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts index 55d125bf993ec6..7b1a554e1d3f12 100644 --- a/x-pack/plugins/security/public/plugin.ts +++ b/x-pack/plugins/security/public/plugin.ts @@ -13,6 +13,8 @@ import { } from './session'; export class SecurityPlugin implements Plugin { + private sessionTimeout!: SessionTimeout; + public setup(core: CoreSetup) { const { http, notifications, injectedMetadata } = core; const { basePath, anonymousPaths } = http; @@ -20,23 +22,25 @@ export class SecurityPlugin implements Plugin; diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 9c0e4cd8036cc9..678c397dfbc64d 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -7,40 +7,81 @@ import { coreMock } from 'src/core/public/mocks'; import { SessionExpired } from './session_expired'; -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -it('redirects user to "/logout" when there is no basePath', async () => { - const { basePath } = coreMock.createSetup().http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); +describe('Session Expiration', () => { + const mockGetItem = jest.fn().mockReturnValue(null); + + beforeAll(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: mockGetItem, + }, + writable: true, }); }); - sessionExpired.logout(); + afterAll(() => { + delete (window as any).sessionStorage; + }); + + describe('logout', () => { + const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); + const tenant = ''; - const url = await newUrlPromise; - expect(url).toBe( - `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); -}); + it('redirects user to "/logout" when there is no basePath', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); -it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { - const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); }); - }); - sessionExpired.logout(); + it('adds a provider parameter when an auth provider is saved in sessionStorage', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + mockGetItem.mockReturnValueOnce('basic'); + + sessionExpired.logout(); - const url = await newUrlPromise; - expect(url).toBe( - `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent( + '/foo/bar?baz=quz#quuz' + )}&msg=SESSION_EXPIRED&provider=basic` + ); + }); + + it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { + const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); + }); + }); }); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 3ef15088bb2889..a43da855267570 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -11,14 +11,19 @@ export interface ISessionExpired { } export class SessionExpired { - constructor(private basePath: HttpSetup['basePath']) {} + constructor(private basePath: HttpSetup['basePath'], private tenant: string) {} logout() { const next = this.basePath.remove( `${window.location.pathname}${window.location.search}${window.location.hash}` ); + const key = `${this.tenant}/session_provider`; + const providerName = sessionStorage.getItem(key); + const provider = providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; window.location.assign( - this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`) + this.basePath.prepend( + `/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED${provider}` + ) ); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx similarity index 71% rename from x-pack/plugins/security/public/session/session_timeout_warning.test.tsx rename to x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx index a52e7ce4e94b52..bb4116420f15da 100644 --- a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; -describe('SessionTimeoutWarning', () => { +describe('SessionIdleTimeoutWarning', () => { it('fires its callback when the OK button is clicked', () => { const handler = jest.fn(); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(handler).toBeCalledTimes(0); wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx new file mode 100644 index 00000000000000..32e4dcc5c6b531 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiProgress } from '@elastic/eui'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + onRefreshSession: () => void; + timeout: number; +} + +export const SessionIdleTimeoutWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+
+ + + +
+ + ); +}; + +export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'warning', + text: toMountPoint( + + ), + title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'clock', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx new file mode 100644 index 00000000000000..7925e92bce4edf --- /dev/null +++ b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { EuiProgress } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + timeout: number; +} + +export const SessionLifespanWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+ + ); +}; + +export const createToast = (toastLifeTimeMs: number): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'danger', + text: toMountPoint(), + title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'alert', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 9917a502790833..df9b8628b180d2 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -8,6 +8,8 @@ import { ISessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { + start: jest.fn(), + stop: jest.fn(), extend: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index 80a22c5fb0b2ae..eb947ab95c43b6 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import BroadcastChannel from 'broadcast-channel'; import { SessionTimeout } from './session_timeout'; import { createSessionExpiredMock } from './session_expired.mock'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -17,25 +18,46 @@ const expectNoWarningToast = ( expect(notifications.toasts.add).not.toHaveBeenCalled(); }; -const expectWarningToast = ( +const expectIdleTimeoutWarningToast = ( notifications: ReturnType['notifications'], - toastLifeTimeMS: number = 60000 + toastLifeTimeMs: number = 60000 ) => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "text": MountPoint { - "reactNode": , - }, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMS}, - }, - ] - `); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "warning", + "iconType": "clock", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); +}; + +const expectLifespanWarningToast = ( + notifications: ReturnType['notifications'], + toastLifeTimeMs: number = 60000 +) => { + expect(notifications.toasts.add).toHaveBeenCalledTimes(1); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "danger", + "iconType": "alert", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); }; const expectWarningToastHidden = ( @@ -46,128 +68,309 @@ const expectWarningToastHidden = ( expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); }; -describe('warning toast', () => { - test(`shows session expiration warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); +describe('Session Timeout', () => { + const now = new Date().getTime(); + const defaultSessionInfo = { + now, + idleTimeoutExpiration: now + 2 * 60 * 1000, + lifespanExpiration: null, + }; + let notifications: ReturnType['notifications']; + let http: ReturnType['http']; + let sessionExpired: ReturnType; + let sessionTimeout: SessionTimeout; + const toast = Symbol(); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + beforeAll(() => { + BroadcastChannel.enforceOptions({ + type: 'simulate', + }); + Object.defineProperty(window, 'sessionStorage', { + value: { + setItem: jest.fn(() => null), + }, + writable: true, + }); }); - test(`extend delays the warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + beforeEach(() => { + const setup = coreMock.createSetup(); + notifications = setup.notifications; + http = setup.http; + notifications.toasts.add.mockReturnValue(toast as any); + sessionExpired = createSessionExpiredMock(); + const tenant = ''; + sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + // default mocked response for checking session info + http.fetch.mockResolvedValue(defaultSessionInfo); + }); - jest.advanceTimersByTime(1 * 1000); + afterEach(async () => { + jest.clearAllMocks(); + }); - expectWarningToast(notifications); + afterAll(() => { + BroadcastChannel.enforceOptions(null); + delete (window as any).sessionStorage; }); - test(`extend hides displayed warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const toast = Symbol(); - notifications.toasts.add.mockReturnValue(toast as any); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('Lifecycle', () => { + test(`starts and initializes on a non-anonymous path`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).not.toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + test(`starts and does not initialize on an anonymous path`, async () => { + http.anonymousPaths.register(window.location.pathname); + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).not.toHaveBeenCalled(); + }); - sessionTimeout.extend(); - expectWarningToastHidden(notifications, toast); - }); + test(`stops`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + const close = jest.fn(sessionTimeout['channel']!.close); + // eslint-disable-next-line dot-notation + sessionTimeout['channel']!.close = close; + // eslint-disable-next-line dot-notation + const cleanup = jest.fn(sessionTimeout['cleanup']); + // eslint-disable-next-line dot-notation + sessionTimeout['cleanup'] = cleanup; - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); - - expect(http.get).not.toHaveBeenCalled(); - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.get).toHaveBeenCalled(); + sessionTimeout.stop(); + expect(close).toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalled(); + }); }); - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(64 * 1000, notifications, sessionExpired, http); + describe('API calls', () => { + const methodName = 'handleSessionInfoAndResetTimers'; + let method: jest.Mock; - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expectWarningToast(notifications, 59 * 1000); - }); -}); + beforeEach(() => { + method = jest.fn(sessionTimeout[methodName]); + sessionTimeout[methodName] = method; + }); -describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + test(`handles success`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); + expect(method).toHaveBeenCalledTimes(1); + }); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`handles error`, async () => { + const mockErrorResponse = new Error('some-error'); + http.fetch.mockRejectedValue(mockErrorResponse); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBeUndefined(); + expect(method).not.toHaveBeenCalled(); + }); }); - test(`extend delays the expiration`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('warning toast', () => { + test(`shows idle timeout warning toast`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectIdleTimeoutWarningToast(notifications); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + test(`shows lifespan warning toast`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); + }); + + test(`extend only results in an HTTP call if a warning is shown`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(54 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectNoWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(10 * 1000); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(4 * 1000, notifications, sessionExpired, http); + expect(http.fetch).toHaveBeenCalledTimes(1); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`extend hides displayed warning toast`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expectIdleTimeoutWarningToast(notifications); + + http.fetch.mockResolvedValue({ + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + expectWarningToastHidden(notifications, toast); + }); + + test(`extend does nothing for session-related routes`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/internal/security/session'); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test(`checks for updated session info before the warning displays`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we check for updated session info 1 second before the warning is shown + const elapsed = 54 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const toastInput = notifications.toasts.add.mock.calls[0][0]; + expect(toastInput).toHaveProperty('text'); + const mountPoint = (toastInput as any).text; + const wrapper = mountWithIntl(mountPoint.__reactMount__); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 64 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalled(); + + jest.advanceTimersByTime(0); + expectIdleTimeoutWarningToast(notifications, 59 * 1000); + }); }); - test(`'null' sessionTimeout never logs you out`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(null, notifications, sessionExpired, http); - sessionTimeout.extend(); - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + describe('session expiration', () => { + test(`expires the session 5 seconds before it really expires`, async () => { + await sessionTimeout.start(); + + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1 * 1000); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`extend delays the expiration`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + const elapsed = 114 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const sessionInfo = { + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); + + // at this point, the session is good for another 120 seconds + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated + // so we need an extra 100ms of padding for this test to ensure that logout has been called + jest.advanceTimersByTime(1 * 1000 + 100); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 4 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`'null' sessionTimeout never logs you out`, async () => { + http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(Number.MAX_VALUE); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index 32302effd6e464..0069e78b5f3722 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsSetup, Toast, HttpSetup } from 'src/core/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; +import { BroadcastChannel } from 'broadcast-channel'; +import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; +import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; +import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -23,58 +23,188 @@ const GRACE_PERIOD_MS = 5 * 1000; */ const WARNING_MS = 60 * 1000; +/** + * Current session info is checked this number of milliseconds before the + * warning toast shows. This will prevent the toast from being shown if the + * session has already been extended. + */ +const SESSION_CHECK_MS = 1000; + +/** + * Route to get session info and extend session expiration + */ +const SESSION_ROUTE = '/internal/security/session'; + export interface ISessionTimeout { - extend(): void; + start(): void; + stop(): void; + extend(url: string): void; } export class SessionTimeout { - private warningTimeoutMilliseconds?: number; - private expirationTimeoutMilliseconds?: number; + private channel?: BroadcastChannel; + private sessionInfo?: SessionInfo; + private fetchTimer?: number; + private warningTimer?: number; + private expirationTimer?: number; private warningToast?: Toast; constructor( - private sessionTimeoutMilliseconds: number | null, private notifications: NotificationsSetup, private sessionExpired: ISessionExpired, - private http: HttpSetup + private http: HttpSetup, + private tenant: string ) {} - extend() { - if (this.sessionTimeoutMilliseconds == null) { + start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (this.warningTimeoutMilliseconds) { - window.clearTimeout(this.warningTimeoutMilliseconds); + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + } + + stop() { + if (this.channel) { + this.channel.close(); } - if (this.expirationTimeoutMilliseconds) { - window.clearTimeout(this.expirationTimeoutMilliseconds); + this.cleanup(); + } + + /** + * When the user makes an authenticated, non-system API call, this function is used to check + * and see if the session has been extended. + * @param url The URL that was called + */ + extend(url: string) { + // avoid an additional API calls when the user clicks the button on the session idle timeout + if (url.endsWith(SESSION_ROUTE)) { + return; } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); + + const { isLifespanTimeout } = this.getTimeout(); + if (this.warningToast && !isLifespanTimeout) { + // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; + // make a new call to get the latest session info + return this.fetchSessionInfoAndResetTimers(); + } + } + + /** + * Fetch latest session information from the server, and optionally attempt to extend + * the session expiration. + */ + private fetchSessionInfoAndResetTimers = async (extend = false) => { + const method = extend ? 'POST' : 'GET'; + const headers = extend ? {} : { 'kbn-system-api': 'true' }; + try { + const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + + this.handleSessionInfoAndResetTimers(result); + + // share this updated session info with any other tabs to sync the UX + if (this.channel) { + this.channel.postMessage(result); + } + } catch (err) { + // do nothing; 401 errors will be caught by the http interceptor + } + }; + + /** + * Processes latest session information, and resets timers based on it. These timers are + * used to trigger an HTTP call for updated session information, to show a timeout + * warning, and to log the user out when their session is expired. + */ + private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { + this.sessionInfo = sessionInfo; + // save the provider name in session storage, we will need it when we log out + const key = `${this.tenant}/session_provider`; + sessionStorage.setItem(key, sessionInfo.provider); + + const { timeout, isLifespanTimeout } = this.getTimeout(); + if (timeout == null) { + return; } - this.warningTimeoutMilliseconds = window.setTimeout( - () => this.showWarning(), - Math.max(this.sessionTimeoutMilliseconds - WARNING_MS - GRACE_PERIOD_MS, 0) + + this.cleanup(); + + // set timers + const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; + if (timeoutVal > 0 && !isLifespanTimeout) { + // we should check for the latest session info before the warning displays + this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal); + } + this.warningTimer = window.setTimeout( + this.showWarning, + Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) ); - this.expirationTimeoutMilliseconds = window.setTimeout( + this.expirationTimer = window.setTimeout( () => this.sessionExpired.logout(), - Math.max(this.sessionTimeoutMilliseconds - GRACE_PERIOD_MS, 0) + Math.max(timeout - GRACE_PERIOD_MS, 0) ); - } + }; - private showWarning = () => { - this.warningToast = this.notifications.toasts.add({ - color: 'warning', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - toastLifeTimeMs: Math.min(this.sessionTimeoutMilliseconds! - GRACE_PERIOD_MS, WARNING_MS), - }); + private cleanup = () => { + if (this.fetchTimer) { + window.clearTimeout(this.fetchTimer); + } + if (this.warningTimer) { + window.clearTimeout(this.warningTimer); + } + if (this.expirationTimer) { + window.clearTimeout(this.expirationTimer); + } + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + } }; - private refreshSession = () => { - this.http.get('/api/security/v1/me'); + /** + * Get the amount of time until the session times out, and whether or not the + * session has reached it maximum lifespan. + */ + private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { + let timeout = null; + let isLifespanTimeout = false; + if (this.sessionInfo) { + const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; + if (idleTimeoutExpiration) { + timeout = idleTimeoutExpiration - now; + } + if ( + lifespanExpiration && + (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) + ) { + timeout = lifespanExpiration - now; + isLifespanTimeout = true; + } + } + return { timeout, isLifespanTimeout }; + }; + + /** + * Show a warning toast depending on the session state. + */ + private showWarning = () => { + const { timeout, isLifespanTimeout } = this.getTimeout(); + const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); + let toast: ToastInput; + if (!isLifespanTimeout) { + const refresh = () => this.fetchSessionInfoAndResetTimers(true); + toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); + } else { + toast = createLifespanToast(toastLifeTimeMs); + } + this.warningToast = this.notifications.toasts.add(toast); }; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 98516cb4a613be..81625e1753b273 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -24,7 +24,7 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpResponse.request.url); } responseError(httpErrorResponse: HttpErrorResponse) { @@ -45,6 +45,6 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpErrorResponse.request.url); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.tsx deleted file mode 100644 index e1b4542031ed1f..00000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - onRefreshSession: () => void; -} - -export const SessionTimeoutWarning = (props: Props) => { - return ( - <> -

- -

-
- - - -
- - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c958..ff2db01cb6c587 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 00000000000000..e9c4b6e281cf3e --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index a58f768f0c7966..1ba98d58a3a5f7 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -74,11 +80,14 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization: 'Basic xxx' }, provider: 'basic', path: mockOptions.basePath.serverBasePath, @@ -292,11 +301,14 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization: 'Basic xxx' }, provider: 'basic', path: mockOptions.basePath.serverBasePath, @@ -419,14 +431,17 @@ describe('Authenticator', () => { expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('properly extends session expiration if it is defined.', async () => { const user = mockAuthenticatedUser(); const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); @@ -449,11 +464,109 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ ...mockSessVal, - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + 3600 * 24, + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('does not extend session lifespan expiration.', async () => { + const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; + + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. + mockOptions = getMockOptions({ + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessVal, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessVal, + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const request = httpServerMock.createKibanaRequest(); @@ -650,11 +763,14 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization: 'Basic xxx' }, provider: 'basic', path: mockOptions.basePath.serverBasePath, @@ -694,6 +810,32 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; @@ -706,4 +848,52 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + path: mockOptions.basePath.serverBasePath, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index d6e3a4e3ad09e7..8f947349cb2e86 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -82,7 +89,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -163,9 +170,14 @@ export class Authenticator { private readonly serverBasePath: string; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -213,7 +225,9 @@ export class Authenticator { ); this.serverBasePath = this.options.basePath.serverBasePath || '/'; - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -268,10 +282,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, path: this.serverBasePath, }); } @@ -327,10 +343,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -346,6 +370,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -422,14 +469,35 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, path: this.serverBasePath, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe010..77f1f9e45aea72 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 7195a4e869e75f..de2fb54ab8c2a1 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -67,19 +67,24 @@ export async function setupAuthentication({ const isValid = (sessionValue: ProviderSession) => { // ensure that this cookie was created with the current Kibana configuration - const { path, expires } = sessionValue; + const { path, idleTimeoutExpiration, lifespanExpiration } = sessionValue; if (path !== undefined && path !== (http.basePath.serverBasePath || '/')) { authLogger.debug(`Outdated session value with path "${sessionValue.path}"`); return false; } // ensure that this cookie is not expired - return !(expires && expires < Date.now()); + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; }; const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ @@ -169,6 +174,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2cd..a6850dcdf8321d 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498b..c5f8f07e50b11c 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 943d582bf484a7..9ddb3e6e96b90b 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -21,8 +21,12 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); @@ -35,8 +39,12 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); @@ -48,8 +56,12 @@ describe('config schema', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": "", "secureCookies": false, - "sessionTimeout": null, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, } `); }); @@ -250,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -270,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -290,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -310,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 6fe3fc73e458c3..c7d990f81369ec 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -26,6 +26,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type) = export const ConfigSchema = schema.object( { + loginAssistanceMessage: schema.string({ defaultValue: '' }), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), @@ -33,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -82,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index b0e2ae71768348..26788c3ef9230a 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -52,8 +55,12 @@ describe('Security Plugin', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -65,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4b3997fe74f1b1..e9566035173497 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -205,7 +208,11 @@ export class Plugin { // We should stop exposing this config as soon as only new platform plugin consumes it. The only // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { - sessionTimeout: config.sessionTimeout, + loginAssistanceMessage: config.loginAssistanceMessage, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb90..086647dcb34597 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 00000000000000..cdebc19d7cf8db --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f83d0c9ea3c9a3..f5fc453557122e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9929,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a830eaacd29e35..288fc92be3cbd8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10018,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", diff --git a/x-pack/tasks/test.ts b/x-pack/tasks/test.ts index d26683899ce3fd..0767d7479724a0 100644 --- a/x-pack/tasks/test.ts +++ b/x-pack/tasks/test.ts @@ -5,35 +5,12 @@ */ import pluginHelpers from '@kbn/plugin-helpers'; -import { createAutoJUnitReporter } from '@kbn/test'; -// @ts-ignore no types available -import mocha from 'gulp-mocha'; import gulp from 'gulp'; import { getEnabledPlugins } from './helpers/flags'; export const testServerTask = async () => { - const pluginIds = await getEnabledPlugins(); - - const testGlobs = ['common/**/__tests__/**/*.js', 'server/**/__tests__/**/*.js']; - - for (const pluginId of pluginIds) { - testGlobs.push( - `legacy/plugins/${pluginId}/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/common/**/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/**/server/**/__tests__/**/*.js` - ); - } - - return gulp.src(testGlobs, { read: false }).pipe( - mocha({ - ui: 'bdd', - require: require.resolve('../../src/setup_node_env'), - reporter: createAutoJUnitReporter({ - reportName: 'X-Pack Mocha Tests', - }), - }) - ); + throw new Error('server mocha tests are now included in the `node scripts/mocha` script'); }; export const testBrowserTask = async () => { @@ -51,4 +28,4 @@ export const testBrowserDevTask = async () => { }); }; -export const testTask = gulp.series(testServerTask, testBrowserTask); +export const testTask = gulp.series(testBrowserTask, testServerTask); diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fce..052d984774e691 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 00000000000000..7c7883f58cb30e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a6..9c67dfe61b957a 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index cb6f0b6028a2d3..f640a34b36ddfb 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,7 +13,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('graph', function() { + // FLAKY: https://github.com/elastic/kibana/issues/45321 + describe.skip('graph', function() { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index ba307a24cd739b..2b76bce544f6d6 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -6,7 +6,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - describe('anomaly detection', function() { + // FLAKY: https://github.com/elastic/kibana/issues/51669 + describe.skip('anomaly detection', function() { loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 1634bea47a69f7..30b957fdf45f4e 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, true, true]); @@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, false, false]); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 49519b530337e6..bfa4be2b067af4 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -15,7 +15,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -89,7 +89,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -160,7 +160,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js index 938324c12a3779..73253224bb45db 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +const { EventEmitter } = require('events'); + import { initRoutes } from './init_routes'; + +const once = function (emitter, event) { + return new Promise(resolve => { + emitter.once(event, resolve); + }); +}; + export default function TaskTestingAPI(kibana) { + const taskTestingEvents = new EventEmitter(); + return new kibana.Plugin({ name: 'sampleTask', require: ['elasticsearch', 'task_manager'], @@ -52,6 +63,10 @@ export default function TaskTestingAPI(kibana) { refresh: true, }); + if (params.waitForEvent) { + await once(taskTestingEvents, params.waitForEvent); + } + return { state: { count: (prevState.count || 0) + 1 }, runAt: millisecondsFromNow(params.nextRunMilliseconds), @@ -88,7 +103,7 @@ export default function TaskTestingAPI(kibana) { }, }); - initRoutes(server); + initRoutes(server, taskTestingEvents); }, }); } diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index a9dfabae6d609f..7b9e265a15d6f8 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -23,11 +23,44 @@ const taskManagerQuery = { } }; -export function initRoutes(server) { +export function initRoutes(server, taskTestingEvents) { const taskManager = server.plugins.task_manager; server.route({ - path: '/api/sample_tasks', + path: '/api/sample_tasks/schedule', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + task: Joi.object({ + taskType: Joi.string().required(), + interval: Joi.string().optional(), + params: Joi.object().required(), + state: Joi.object().optional(), + id: Joi.string().optional() + }) + }), + }, + }, + async handler(request) { + try { + const { task: taskFields } = request.payload; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await (taskManager.schedule(task, { request })); + + return taskResult; + } catch (err) { + return err; + } + }, + }); + + server.route({ + path: '/api/sample_tasks/ensure_scheduled', method: 'POST', config: { validate: { @@ -38,26 +71,19 @@ export function initRoutes(server) { params: Joi.object().required(), state: Joi.object().optional(), id: Joi.string().optional() - }), - ensureScheduled: Joi.boolean() - .default(false) - .optional(), + }) }), }, }, async handler(request) { try { - const { ensureScheduled = false, task: taskFields } = request.payload; + const { task: taskFields } = request.payload; const task = { ...taskFields, scope: [scope], }; - const taskResult = await ( - ensureScheduled - ? taskManager.ensureScheduled(task, { request }) - : taskManager.schedule(task, { request }) - ); + const taskResult = await (taskManager.ensureScheduled(task, { request })); return taskResult; } catch (err) { @@ -66,6 +92,27 @@ export function initRoutes(server) { }, }); + server.route({ + path: '/api/sample_tasks/event', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + event: Joi.string().required() + }), + }, + }, + async handler(request) { + try { + const { event } = request.payload; + taskTestingEvents.emit(event); + return { event }; + } catch (err) { + return err; + } + }, + }); + server.route({ path: '/api/sample_tasks', method: 'GET', diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 9b4297e995cbd8..986648f795da65 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -58,7 +58,7 @@ export default function ({ getService }) { } function scheduleTask(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/schedule') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) @@ -66,13 +66,20 @@ export default function ({ getService }) { } function scheduleTaskIfNotExists(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/ensure_scheduled') .set('kbn-xsrf', 'xxx') - .send({ task, ensureScheduled: true }) + .send({ task }) .expect(200) .then((response) => response.body); } + function releaseTasksWaitingForEventToComplete(event) { + return supertest.post('/api/sample_tasks/event') + .set('kbn-xsrf', 'xxx') + .send({ event }) + .expect(200); + } + it('should support middleware', async () => { const historyItem = _.random(1, 100); @@ -204,5 +211,45 @@ export default function ({ getService }) { expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.greaterThan(expectedDiff - buffer); expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); } + + it('should run tasks in parallel, allowing for long running tasks along side faster tasks', async () => { + /** + * It's worth noting this test relies on the /event endpoint that forces Task Manager to hold off + * on completing a task until a call is made by the test suite. + * If we begin testing with multiple Kibana instacnes in Parallel this will likely become flaky. + * If you end up here because the test is flaky, this might be why. + */ + const fastTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { }, + }); + + const longRunningTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { + waitForEvent: 'rescheduleHasHappened' + }, + }); + + function getTaskById(tasks, id) { + return tasks.filter(task => task.id === id)[0]; + } + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(getTaskById(tasks, fastTask.id).state.count).to.eql(2); + }); + + await releaseTasksWaitingForEventToComplete('rescheduleHasHappened'); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + + expect(getTaskById(tasks, fastTask.id).state.count).to.greaterThan(2); + expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); + }); + }); }); } diff --git a/yarn.lock b/yarn.lock index 3296fc013c48d2..7e965979fd46ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,7 +1004,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -6448,6 +6448,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,6 +6784,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -9482,11 +9500,6 @@ dargs@^5.1.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" integrity sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk= -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -11651,21 +11664,6 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" - integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== - dependencies: - cross-spawn "^6.0.5" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" @@ -14225,18 +14223,6 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" -gulp-mocha@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408" - integrity sha512-ZXBGN60TXYnFhttr19mfZBOtlHYGx9SvCSc+Kr/m2cMIGloUe176HBPwvPqlakPuQgeTGVRS47NmcdZUereKMQ== - dependencies: - dargs "^7.0.0" - execa "^2.0.4" - mocha "^6.2.0" - plugin-error "^1.0.1" - supports-color "^7.0.0" - through2 "^3.0.1" - gulp-rename@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd" @@ -16970,6 +16956,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -19029,6 +19020,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19352,35 +19348,6 @@ mocha-junit-reporter@^1.23.1: strip-ansi "^4.0.0" xml "^1.0.0" -mocha@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" - integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "2.2.0" - minimatch "3.0.4" - mkdirp "0.5.1" - ms "2.1.1" - node-environment-flags "1.0.5" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.0" - yargs-parser "13.1.1" - yargs-unparser "1.6.0" - mocha@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" @@ -19638,6 +19605,13 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -20193,13 +20167,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - npm-run-path@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" @@ -24521,6 +24488,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24528,13 +24502,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28253,6 +28220,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"