diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 517aefb36e8d6..4058fcaadee5d 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -13,8 +13,9 @@ jobs: with: issue-mappings: | [ - { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, - { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, - { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} + +# { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, +# { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, +# { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } diff --git a/docs/development/core/server/kibana-plugin-server.authnothandled.md b/docs/development/core/server/kibana-plugin-server.authnothandled.md new file mode 100644 index 0000000000000..01e465c266319 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authnothandled.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthNotHandled](./kibana-plugin-server.authnothandled.md) + +## AuthNotHandled interface + + +Signature: + +```typescript +export interface AuthNotHandled +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.authnothandled.type.md) | AuthResultType.notHandled | | + diff --git a/docs/development/core/server/kibana-plugin-server.authnothandled.type.md b/docs/development/core/server/kibana-plugin-server.authnothandled.type.md new file mode 100644 index 0000000000000..81543de0ec61b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authnothandled.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthNotHandled](./kibana-plugin-server.authnothandled.md) > [type](./kibana-plugin-server.authnothandled.type.md) + +## AuthNotHandled.type property + +Signature: + +```typescript +type: AuthResultType.notHandled; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirected.md b/docs/development/core/server/kibana-plugin-server.authredirected.md new file mode 100644 index 0000000000000..3eb88d6c5a230 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirected.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirected](./kibana-plugin-server.authredirected.md) + +## AuthRedirected interface + + +Signature: + +```typescript +export interface AuthRedirected extends AuthRedirectedParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.authredirected.type.md) | AuthResultType.redirected | | + diff --git a/docs/development/core/server/kibana-plugin-server.authredirected.type.md b/docs/development/core/server/kibana-plugin-server.authredirected.type.md new file mode 100644 index 0000000000000..866ed358119e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirected.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirected](./kibana-plugin-server.authredirected.md) > [type](./kibana-plugin-server.authredirected.type.md) + +## AuthRedirected.type property + +Signature: + +```typescript +type: AuthResultType.redirected; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md b/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md new file mode 100644 index 0000000000000..c1cf8218e7509 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) > [headers](./kibana-plugin-server.authredirectedparams.headers.md) + +## AuthRedirectedParams.headers property + +Headers to attach for auth redirect. Must include "location" header + +Signature: + +```typescript +headers: { + location: string; + } & ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirectedparams.md b/docs/development/core/server/kibana-plugin-server.authredirectedparams.md new file mode 100644 index 0000000000000..3658f88fb6495 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirectedparams.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) + +## AuthRedirectedParams interface + +Result of auth redirection. + +Signature: + +```typescript +export interface AuthRedirectedParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.authredirectedparams.headers.md) | {
location: string;
} & ResponseHeaders | Headers to attach for auth redirect. Must include "location" header | + diff --git a/docs/development/core/server/kibana-plugin-server.authresult.md b/docs/development/core/server/kibana-plugin-server.authresult.md index 8739c4899bd02..f540173f34c7c 100644 --- a/docs/development/core/server/kibana-plugin-server.authresult.md +++ b/docs/development/core/server/kibana-plugin-server.authresult.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthResult = Authenticated; +export declare type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.md b/docs/development/core/server/kibana-plugin-server.authresultparams.md index 55b247f21f5a9..7a725cb340f5b 100644 --- a/docs/development/core/server/kibana-plugin-server.authresultparams.md +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.md @@ -4,7 +4,7 @@ ## AuthResultParams interface -Result of an incoming request authentication. +Result of successful authentication. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.authresulttype.md b/docs/development/core/server/kibana-plugin-server.authresulttype.md index 61a98ee5e7b11..48c159a94c23d 100644 --- a/docs/development/core/server/kibana-plugin-server.authresulttype.md +++ b/docs/development/core/server/kibana-plugin-server.authresulttype.md @@ -16,4 +16,6 @@ export declare enum AuthResultType | Member | Value | Description | | --- | --- | --- | | authenticated | "authenticated" | | +| notHandled | "notHandled" | | +| redirected | "redirected" | | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index bc7003c5a68f3..a6a30dae894ad 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -17,4 +17,6 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | | [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true | +| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (headers: {
location: string;
} & ResponseHeaders) => AuthResult | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md new file mode 100644 index 0000000000000..7de174b3c7bb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) + +## AuthToolkit.notHandled property + +User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true + +Signature: + +```typescript +notHandled: () => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md new file mode 100644 index 0000000000000..64d1d04a4abc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) + +## AuthToolkit.redirected property + +Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' + +Signature: + +```typescript +redirected: (headers: { + location: string; + } & ResponseHeaders) => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md new file mode 100644 index 0000000000000..536d6bd04d937 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [auth](./kibana-plugin-server.kibanarequest.auth.md) + +## KibanaRequest.auth property + +Signature: + +```typescript +readonly auth: { + isAuthenticated: boolean; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index cb6745623e381..0d520783fd4cf 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -22,6 +22,7 @@ export declare class KibanaRequest{
isAuthenticated: boolean;
} | | | [body](./kibana-plugin-server.kibanarequest.body.md) | | Body | | | [events](./kibana-plugin-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) | | [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 2a388c6b79fad..ff243dbb91a89 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -53,7 +53,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AssistanceAPIResponse](./kibana-plugin-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-server.assistantapiclientparams.md) | | | [Authenticated](./kibana-plugin-server.authenticated.md) | | -| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. | +| [AuthNotHandled](./kibana-plugin-server.authnothandled.md) | | +| [AuthRedirected](./kibana-plugin-server.authredirected.md) | | +| [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) | Result of auth redirection. | +| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of successful authentication. | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md index e4cbca9c97810..830abd4dde738 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md @@ -4,12 +4,12 @@ ## RouteConfigOptions.authRequired property -A flag shows that authentication for a route: `enabled` when true `disabled` when false +Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. -Enabled by default. +Defaults to `true` if an auth mechanism is registered. Signature: ```typescript -authRequired?: boolean; +authRequired?: boolean | 'optional'; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 7fbab90cc2c8a..6664a28424a32 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -16,7 +16,7 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | | [body](./kibana-plugin-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | | [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | diff --git a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap b/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap deleted file mode 100644 index 97e9082401b3d..0000000000000 --- a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]."`; - -exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`; - -exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`; - -exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`; - -exports[`parsing units throws an error when unsupported unit specified 1`] = `"Failed to parse [1tb] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/byte_size_value/index.test.ts b/packages/kbn-config-schema/src/byte_size_value/index.test.ts index 198d95aa0ab4c..59960a4567f60 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.test.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.test.ts @@ -42,19 +42,29 @@ describe('parsing units', () => { }); test('throws an error when unsupported unit specified', () => { - expect(() => ByteSizeValue.parse('1tb')).toThrowErrorMatchingSnapshot(); + expect(() => ByteSizeValue.parse('1tb')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse value as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."` + ); }); }); describe('#constructor', () => { test('throws if number of bytes is negative', () => { - expect(() => new ByteSizeValue(-1024)).toThrowErrorMatchingSnapshot(); + expect(() => new ByteSizeValue(-1024)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); }); test('throws if number of bytes is not safe', () => { - expect(() => new ByteSizeValue(NaN)).toThrowErrorMatchingSnapshot(); - expect(() => new ByteSizeValue(Infinity)).toThrowErrorMatchingSnapshot(); - expect(() => new ByteSizeValue(Math.pow(2, 53))).toThrowErrorMatchingSnapshot(); + expect(() => new ByteSizeValue(NaN)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); + expect(() => new ByteSizeValue(Infinity)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); + expect(() => new ByteSizeValue(Math.pow(2, 53))).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); }); test('accepts 0', () => { diff --git a/packages/kbn-config-schema/src/byte_size_value/index.ts b/packages/kbn-config-schema/src/byte_size_value/index.ts index 48862821bb78d..183a6c30f3839 100644 --- a/packages/kbn-config-schema/src/byte_size_value/index.ts +++ b/packages/kbn-config-schema/src/byte_size_value/index.ts @@ -38,7 +38,7 @@ export class ByteSizeValue { const number = Number(text); if (typeof number !== 'number' || isNaN(number)) { throw new Error( - `Failed to parse [${text}] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] ` + + `Failed to parse value as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] ` + `(e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer.` ); } @@ -53,9 +53,7 @@ export class ByteSizeValue { constructor(private readonly valueInBytes: number) { if (!Number.isSafeInteger(valueInBytes) || valueInBytes < 0) { - throw new Error( - `Value in bytes is expected to be a safe positive integer, but provided [${valueInBytes}].` - ); + throw new Error(`Value in bytes is expected to be a safe positive integer.`); } } diff --git a/packages/kbn-config-schema/src/duration/index.ts b/packages/kbn-config-schema/src/duration/index.ts index b96b5a3687bbb..282c150e8150a 100644 --- a/packages/kbn-config-schema/src/duration/index.ts +++ b/packages/kbn-config-schema/src/duration/index.ts @@ -28,7 +28,7 @@ function stringToDuration(text: string) { const number = Number(text); if (typeof number !== 'number' || isNaN(number)) { throw new Error( - `Failed to parse [${text}] as time value. Value must be a duration in milliseconds, or follow the format ` + + `Failed to parse value as time value. Value must be a duration in milliseconds, or follow the format ` + `[ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer.` ); } @@ -43,9 +43,7 @@ function stringToDuration(text: string) { function numberToDuration(numberMs: number) { if (!Number.isSafeInteger(numberMs) || numberMs < 0) { - throw new Error( - `Value in milliseconds is expected to be a safe positive integer, but provided [${numberMs}].` - ); + throw new Error(`Value in milliseconds is expected to be a safe positive integer.`); } return momentDuration(numberMs); diff --git a/packages/kbn-config-schema/src/types/__snapshots__/any_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/any_type.test.ts.snap deleted file mode 100644 index 3a40752d52b6e..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/any_type.test.ts.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [any] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [any] but got [undefined]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap deleted file mode 100644 index 0e5f6de2deea8..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [boolean] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [boolean] but got [undefined]"`; - -exports[`returns error when not boolean 1`] = `"expected value of type [boolean] but got [number]"`; - -exports[`returns error when not boolean 2`] = `"expected value of type [boolean] but got [Array]"`; - -exports[`returns error when not boolean 3`] = `"expected value of type [boolean] but got [string]"`; - -exports[`returns error when not boolean 4`] = `"expected value of type [boolean] but got [number]"`; - -exports[`returns error when not boolean 5`] = `"expected value of type [boolean] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap deleted file mode 100644 index 96a7ab34dac26..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Buffer] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`; - -exports[`returns error when not a buffer 1`] = `"expected value of type [Buffer] but got [number]"`; - -exports[`returns error when not a buffer 2`] = `"expected value of type [Buffer] but got [Array]"`; - -exports[`returns error when not a buffer 3`] = `"expected value of type [Buffer] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap deleted file mode 100644 index ea2102b1776fb..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#defaultValue can be a ByteSizeValue 1`] = ` -ByteSizeValue { - "valueInBytes": 1024, -} -`; - -exports[`#defaultValue can be a number 1`] = ` -ByteSizeValue { - "valueInBytes": 1024, -} -`; - -exports[`#defaultValue can be a string 1`] = ` -ByteSizeValue { - "valueInBytes": 1024, -} -`; - -exports[`#defaultValue can be a string-formatted number 1`] = ` -ByteSizeValue { - "valueInBytes": 1024, -} -`; - -exports[`#max returns error when larger 1`] = `"Value is [1mb] ([1048576b]) but it must be equal to or less than [1kb]"`; - -exports[`#max returns value when smaller 1`] = ` -ByteSizeValue { - "valueInBytes": 1, -} -`; - -exports[`#min returns error when smaller 1`] = `"Value is [1b] ([1b]) but it must be equal to or greater than [1kb]"`; - -exports[`#min returns value when larger 1`] = ` -ByteSizeValue { - "valueInBytes": 1024, -} -`; - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [ByteSize] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [ByteSize] but got [undefined]"`; - -exports[`returns error when not valid string or positive safe integer 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-123]."`; - -exports[`returns error when not valid string or positive safe integer 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`; - -exports[`returns error when not valid string or positive safe integer 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`; - -exports[`returns error when not valid string or positive safe integer 4`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`; - -exports[`returns error when not valid string or positive safe integer 5`] = `"expected value of type [ByteSize] but got [Array]"`; - -exports[`returns error when not valid string or positive safe integer 6`] = `"expected value of type [ByteSize] but got [RegExp]"`; - -exports[`returns error when not valid string or positive safe integer 7`] = `"Failed to parse [123foo] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; - -exports[`returns error when not valid string or positive safe integer 8`] = `"Failed to parse [123 456] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap deleted file mode 100644 index b32db114860f5..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/conditional_type.test.ts.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`correctly handles missing references 1`] = `"[value]: expected value of type [string] but got [number]"`; - -exports[`includes namespace into failures 1`] = `"[mega-namespace.value]: expected value of type [string] but got [number]"`; - -exports[`includes namespace into failures 2`] = `"[mega-namespace.value]: expected value of type [number] but got [string]"`; - -exports[`properly handles conditionals within objects 1`] = `"[value]: expected value of type [string] but got [number]"`; - -exports[`properly handles conditionals within objects 2`] = `"[value]: expected value of type [number] but got [string]"`; - -exports[`properly handles schemas with incompatible types 1`] = `"expected value of type [string] but got [boolean]"`; - -exports[`properly handles schemas with incompatible types 2`] = `"expected value of type [boolean] but got [string]"`; - -exports[`properly validates types according chosen schema 1`] = `"value is [a] but it must have a minimum length of [2]."`; - -exports[`properly validates types according chosen schema 2`] = `"value is [ab] but it must have a maximum length of [1]."`; - -exports[`properly validates when compares with "null" literal Schema 1`] = `"value is [a] but it must have a minimum length of [2]."`; - -exports[`properly validates when compares with "null" literal Schema 2`] = `"value is [ab] but it must have a minimum length of [3]."`; - -exports[`properly validates when compares with Schema 1`] = `"value is [a] but it must have a minimum length of [2]."`; - -exports[`properly validates when compares with Schema 2`] = `"value is [ab] but it must have a minimum length of [3]."`; - -exports[`required by default 1`] = `"expected value of type [string] but got [undefined]"`; - -exports[`works with both context and sibling references 1`] = `"[value]: expected value of type [string] but got [number]"`; - -exports[`works with both context and sibling references 2`] = `"[value]: expected value of type [number] but got [string]"`; - -exports[`works within \`oneOf\` 1`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [number] -- [1]: expected value of type [array] but got [number]" -`; - -exports[`works within \`oneOf\` 2`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean] -- [1]: expected value of type [array] but got [boolean]" -`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap deleted file mode 100644 index c4e4ff652a2d7..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#defaultValue can be a moment.Duration 1`] = `"PT1H"`; - -exports[`#defaultValue can be a number 1`] = `"PT0.6S"`; - -exports[`#defaultValue can be a string 1`] = `"PT1H"`; - -exports[`#defaultValue can be a string-formatted number 1`] = `"PT0.6S"`; - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [moment.Duration] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [moment.Duration] but got [undefined]"`; - -exports[`returns error when not valid string or non-safe positive integer 1`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [-123]."`; - -exports[`returns error when not valid string or non-safe positive integer 2`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [NaN]."`; - -exports[`returns error when not valid string or non-safe positive integer 3`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [Infinity]."`; - -exports[`returns error when not valid string or non-safe positive integer 4`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [9007199254740992]."`; - -exports[`returns error when not valid string or non-safe positive integer 5`] = `"expected value of type [moment.Duration] but got [Array]"`; - -exports[`returns error when not valid string or non-safe positive integer 6`] = `"expected value of type [moment.Duration] but got [RegExp]"`; - -exports[`returns error when not valid string or non-safe positive integer 7`] = `"Failed to parse [123foo] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`; - -exports[`returns error when not valid string or non-safe positive integer 8`] = `"Failed to parse [123 456] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap deleted file mode 100644 index 14d474b4a516b..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/literal_type.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value to equal [test] but got [foo]"`; - -exports[`returns error when not correct 1`] = `"expected value to equal [test] but got [foo]"`; - -exports[`returns error when not correct 2`] = `"expected value to equal [true] but got [false]"`; - -exports[`returns error when not correct 3`] = `"expected value to equal [test] but got [1,2,3]"`; - -exports[`returns error when not correct 4`] = `"expected value to equal [123] but got [abc]"`; - -exports[`returns error when not correct 5`] = `"expected value to equal [null] but got [42]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap deleted file mode 100644 index fdb172df356a7..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails if null 1`] = `"expected value of type [string] but got [null]"`; - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [string] but got [null]"`; - -exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; - -exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap deleted file mode 100644 index 6eea2a7cefc72..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/never_type.test.ts.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`throws on any value set 1`] = `"a value wasn't expected to be present"`; - -exports[`throws on any value set 2`] = `"a value wasn't expected to be present"`; - -exports[`throws on any value set 3`] = `"a value wasn't expected to be present"`; - -exports[`throws on any value set 4`] = `"a value wasn't expected to be present"`; - -exports[`throws on value set as object property 1`] = `"[name]: a value wasn't expected to be present"`; - -exports[`works for conditional types 1`] = `"[name]: a value wasn't expected to be present"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap deleted file mode 100644 index 96ab664921fdf..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = ` -"[foo-namespace]: types that failed validation: -- [foo-namespace.0]: value is [foo] but it must have a maximum length of [1]. -- [foo-namespace.1]: expected value to equal [null] but got [foo]" -`; - -exports[`validates basic type 1`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [number] -- [1]: expected value to equal [null] but got [666]" -`; - -exports[`validates contained type 1`] = ` -"types that failed validation: -- [0]: value is [foo] but it must have a maximum length of [1]. -- [1]: expected value to equal [null] but got [foo]" -`; - -exports[`validates type errors in object 1`] = ` -"[foo]: types that failed validation: -- [foo.0]: value is [ab] but it must have a maximum length of [1]. -- [foo.1]: expected value to equal [null] but got [ab]" -`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/number_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/number_type.test.ts.snap deleted file mode 100644 index 5d1e5fcf1ef81..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/number_type.test.ts.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#max returns error when larger number 1`] = `"Value is [3] but it must be equal to or lower than [2]."`; - -exports[`#min returns error when smaller number 1`] = `"Value is [3] but it must be equal to or greater than [4]."`; - -exports[`fails if number is \`NaN\` 1`] = `"expected value of type [number] but got [number]"`; - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [number] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [number] but got [undefined]"`; - -exports[`returns error when not number or numeric string 1`] = `"expected value of type [number] but got [string]"`; - -exports[`returns error when not number or numeric string 2`] = `"expected value of type [number] but got [Array]"`; - -exports[`returns error when not number or numeric string 3`] = `"expected value of type [number] but got [RegExp]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/one_of_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/one_of_type.test.ts.snap deleted file mode 100644 index 75dfff456ebe7..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/one_of_type.test.ts.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails if not matching literal 1`] = ` -"types that failed validation: -- [0]: expected value to equal [foo] but got [bar]" -`; - -exports[`fails if not matching multiple types 1`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean] -- [1]: expected value of type [number] but got [boolean]" -`; - -exports[`fails if not matching type 1`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [boolean]" -`; - -exports[`fails if not matching type 2`] = ` -"types that failed validation: -- [0]: expected value of type [string] but got [number]" -`; - -exports[`handles object with wrong type 1`] = ` -"types that failed validation: -- [0.age]: expected value of type [number] but got [string]" -`; - -exports[`includes namespace in failure 1`] = ` -"[foo-namespace]: types that failed validation: -- [foo-namespace.0.age]: expected value of type [number] but got [string]" -`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap deleted file mode 100644 index e813b4f68a09e..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Stream] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`; - -exports[`returns error when not a stream 1`] = `"expected value of type [Stream] but got [number]"`; - -exports[`returns error when not a stream 2`] = `"expected value of type [Stream] but got [Array]"`; - -exports[`returns error when not a stream 3`] = `"expected value of type [Stream] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/string_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/string_type.test.ts.snap deleted file mode 100644 index 8e1f63fb66733..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/string_type.test.ts.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#hostname returns error when empty string 1`] = `"any.empty"`; - -exports[`#hostname returns error when value is not a valid hostname 1`] = `"value is [host:name] but it must be a valid hostname (see RFC 1123)."`; - -exports[`#hostname returns error when value is not a valid hostname 2`] = `"value is [localhost:5601] but it must be a valid hostname (see RFC 1123)."`; - -exports[`#hostname returns error when value is not a valid hostname 3`] = `"value is [-] but it must be a valid hostname (see RFC 1123)."`; - -exports[`#hostname returns error when value is not a valid hostname 4`] = `"value is [0:?:0:0:0:0:0:1] but it must be a valid hostname (see RFC 1123)."`; - -exports[`#hostname returns error when value is not a valid hostname 5`] = `"value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must be a valid hostname (see RFC 1123)."`; - -exports[`#hostname supports string validation rules 1`] = `"value is [www.example.com] but it must have a maximum length of [3]."`; - -exports[`#maxLength returns error when longer string 1`] = `"value is [foo] but it must have a maximum length of [2]."`; - -exports[`#minLength returns error when empty string 1`] = `"value is [] but it must have a minimum length of [2]."`; - -exports[`#minLength returns error when shorter string 1`] = `"value is [foo] but it must have a minimum length of [4]."`; - -exports[`#validate throw when empty string 1`] = `"validator failure"`; - -exports[`#validate throws when returns string 1`] = `"validator failure"`; - -exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [string] but got [undefined]"`; - -exports[`is required by default 1`] = `"expected value of type [string] but got [undefined]"`; - -exports[`returns error when not string 1`] = `"expected value of type [string] but got [number]"`; - -exports[`returns error when not string 2`] = `"expected value of type [string] but got [Array]"`; - -exports[`returns error when not string 3`] = `"expected value of type [string] but got [RegExp]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap deleted file mode 100644 index 81fafdb4a7b33..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but got [ftp://elastic.co]."`; - -exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but got [file:///kibana.log]."`; - -exports[`#validate throws when returns string 1`] = `"validator failure"`; - -exports[`is required by default 1`] = `"expected value of type [string] but got [undefined]."`; - -exports[`returns error when not string 1`] = `"expected value of type [string] but got [number]."`; - -exports[`returns error when not string 2`] = `"expected value of type [string] but got [Array]."`; - -exports[`returns error when not string 3`] = `"expected value of type [string] but got [RegExp]."`; - -exports[`returns error when value is not a URI 1`] = `"value is [3domain.local] but it must be a valid URI (see RFC 3986)."`; - -exports[`returns error when value is not a URI 2`] = `"value is [http://8010:0:0:0:9:500:300C:200A] but it must be a valid URI (see RFC 3986)."`; - -exports[`returns error when value is not a URI 3`] = `"value is [-] but it must be a valid URI (see RFC 3986)."`; - -exports[`returns error when value is not a URI 4`] = `"value is [https://example.com?baz[]=foo&baz[]=bar] but it must be a valid URI (see RFC 3986)."`; - -exports[`returns error when value is not a URI 5`] = `"value is [http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must be a valid URI (see RFC 3986)."`; diff --git a/packages/kbn-config-schema/src/types/any_type.test.ts b/packages/kbn-config-schema/src/types/any_type.test.ts index 4d68c860ba13d..30a9a8b71ea12 100644 --- a/packages/kbn-config-schema/src/types/any_type.test.ts +++ b/packages/kbn-config-schema/src/types/any_type.test.ts @@ -28,13 +28,17 @@ test('works for any value', () => { }); test('is required by default', () => { - expect(() => schema.any().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.any().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [any] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.any().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [any] but got [undefined]"` + ); }); describe('#defaultValue', () => { diff --git a/packages/kbn-config-schema/src/types/array_type.test.ts b/packages/kbn-config-schema/src/types/array_type.test.ts index 66b72096a593d..9f3370de8c265 100644 --- a/packages/kbn-config-schema/src/types/array_type.test.ts +++ b/packages/kbn-config-schema/src/types/array_type.test.ts @@ -39,7 +39,7 @@ test('fails if wrong input type', () => { test('fails if string input cannot be parsed', () => { const type = schema.arrayOf(schema.string()); expect(() => type.validate('test')).toThrowErrorMatchingInlineSnapshot( - `"could not parse array value from [test]"` + `"could not parse array value from json input"` ); }); @@ -53,7 +53,7 @@ test('fails with correct type if parsed input is not an array', () => { test('includes namespace in failure when wrong top-level type', () => { const type = schema.arrayOf(schema.string()); expect(() => type.validate('test', {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( - `"[foo-namespace]: could not parse array value from [test]"` + `"[foo-namespace]: could not parse array value from json input"` ); }); diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts index a0353e8348ddd..0df0d44a37951 100644 --- a/packages/kbn-config-schema/src/types/array_type.ts +++ b/packages/kbn-config-schema/src/types/array_type.ts @@ -52,7 +52,7 @@ export class ArrayType extends Type { case 'array.sparse': return `sparse array are not allowed`; case 'array.parse': - return `could not parse array value from [${value}]`; + return `could not parse array value from json input`; case 'array.min': return `array size is [${value.length}], but cannot be smaller than [${limit}]`; case 'array.max': diff --git a/packages/kbn-config-schema/src/types/boolean_type.test.ts b/packages/kbn-config-schema/src/types/boolean_type.test.ts index e94999b505437..bffa3e18f93bf 100644 --- a/packages/kbn-config-schema/src/types/boolean_type.test.ts +++ b/packages/kbn-config-schema/src/types/boolean_type.test.ts @@ -35,13 +35,17 @@ test('handles boolean strings', () => { }); test('is required by default', () => { - expect(() => schema.boolean().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.boolean().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [boolean] but got [undefined]"` + ); }); describe('#defaultValue', () => { @@ -55,13 +59,23 @@ describe('#defaultValue', () => { }); test('returns error when not boolean', () => { - expect(() => schema.boolean().validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); - expect(() => schema.boolean().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [Array]"` + ); - expect(() => schema.boolean().validate('abc')).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate('abc')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); - expect(() => schema.boolean().validate(0)).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate(0)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); - expect(() => schema.boolean().validate('no')).toThrowErrorMatchingSnapshot(); + expect(() => schema.boolean().validate('no')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); }); diff --git a/packages/kbn-config-schema/src/types/buffer_type.test.ts b/packages/kbn-config-schema/src/types/buffer_type.test.ts index 63d59296aec84..a7b589df0c6d1 100644 --- a/packages/kbn-config-schema/src/types/buffer_type.test.ts +++ b/packages/kbn-config-schema/src/types/buffer_type.test.ts @@ -25,13 +25,17 @@ test('returns value by default', () => { }); test('is required by default', () => { - expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Buffer] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.buffer().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [Buffer] but got [undefined]"` + ); }); describe('#defaultValue', () => { @@ -49,9 +53,15 @@ describe('#defaultValue', () => { }); test('returns error when not a buffer', () => { - expect(() => schema.buffer().validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => schema.buffer().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Buffer] but got [number]"` + ); - expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Buffer] but got [Array]"` + ); - expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingSnapshot(); + expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Buffer] but got [string]"` + ); }); diff --git a/packages/kbn-config-schema/src/types/byte_size_type.test.ts b/packages/kbn-config-schema/src/types/byte_size_type.test.ts index 7c65ec2945b49..a69a7296a6eb8 100644 --- a/packages/kbn-config-schema/src/types/byte_size_type.test.ts +++ b/packages/kbn-config-schema/src/types/byte_size_type.test.ts @@ -35,11 +35,17 @@ test('handles numbers', () => { }); test('is required by default', () => { - expect(() => byteSize().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [ByteSize] but got [undefined]"` + ); }); test('includes namespace in failure', () => { - expect(() => byteSize().validate(undefined, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => + byteSize().validate(undefined, {}, 'foo-namespace') + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [ByteSize] but got [undefined]"` + ); }); describe('#defaultValue', () => { @@ -48,7 +54,11 @@ describe('#defaultValue', () => { byteSize({ defaultValue: ByteSizeValue.parse('1kb'), }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1024, + } + `); }); test('can be a string', () => { @@ -56,7 +66,11 @@ describe('#defaultValue', () => { byteSize({ defaultValue: '1kb', }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1024, + } + `); }); test('can be a string-formatted number', () => { @@ -64,7 +78,11 @@ describe('#defaultValue', () => { byteSize({ defaultValue: '1024', }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1024, + } + `); }); test('can be a number', () => { @@ -72,7 +90,11 @@ describe('#defaultValue', () => { byteSize({ defaultValue: 1024, }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1024, + } + `); }); }); @@ -82,7 +104,11 @@ describe('#min', () => { byteSize({ min: '1b', }).validate('1kb') - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1024, + } + `); }); test('returns error when smaller', () => { @@ -90,34 +116,56 @@ describe('#min', () => { byteSize({ min: '1kb', }).validate('1b') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"Value must be equal to or greater than [1kb]"`); }); }); describe('#max', () => { test('returns value when smaller', () => { - expect(byteSize({ max: '1kb' }).validate('1b')).toMatchSnapshot(); + expect(byteSize({ max: '1kb' }).validate('1b')).toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 1, + } + `); }); test('returns error when larger', () => { - expect(() => byteSize({ max: '1kb' }).validate('1mb')).toThrowErrorMatchingSnapshot(); + expect(() => byteSize({ max: '1kb' }).validate('1mb')).toThrowErrorMatchingInlineSnapshot( + `"Value must be equal to or less than [1kb]"` + ); }); }); test('returns error when not valid string or positive safe integer', () => { - expect(() => byteSize().validate(-123)).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(-123)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); - expect(() => byteSize().validate(NaN)).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(NaN)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); - expect(() => byteSize().validate(Infinity)).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(Infinity)).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); - expect(() => byteSize().validate(Math.pow(2, 53))).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(Math.pow(2, 53))).toThrowErrorMatchingInlineSnapshot( + `"Value in bytes is expected to be a safe positive integer."` + ); - expect(() => byteSize().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [ByteSize] but got [Array]"` + ); - expect(() => byteSize().validate(/abc/)).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [ByteSize] but got [RegExp]"` + ); - expect(() => byteSize().validate('123foo')).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate('123foo')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse value as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."` + ); - expect(() => byteSize().validate('123 456')).toThrowErrorMatchingSnapshot(); + expect(() => byteSize().validate('123 456')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse value as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."` + ); }); diff --git a/packages/kbn-config-schema/src/types/byte_size_type.ts b/packages/kbn-config-schema/src/types/byte_size_type.ts index 4833de7ecf15f..f7aa12291c7de 100644 --- a/packages/kbn-config-schema/src/types/byte_size_type.ts +++ b/packages/kbn-config-schema/src/types/byte_size_type.ts @@ -61,13 +61,9 @@ export class ByteSizeType extends Type { case 'bytes.parse': return new SchemaTypeError(message, path); case 'bytes.min': - return `Value is [${value.toString()}] ([${value.toString( - 'b' - )}]) but it must be equal to or greater than [${limit.toString()}]`; + return `Value must be equal to or greater than [${limit.toString()}]`; case 'bytes.max': - return `Value is [${value.toString()}] ([${value.toString( - 'b' - )}]) but it must be equal to or less than [${limit.toString()}]`; + return `Value must be equal to or less than [${limit.toString()}]`; } } } diff --git a/packages/kbn-config-schema/src/types/conditional_type.test.ts b/packages/kbn-config-schema/src/types/conditional_type.test.ts index 354854b864755..b7ad431318e85 100644 --- a/packages/kbn-config-schema/src/types/conditional_type.test.ts +++ b/packages/kbn-config-schema/src/types/conditional_type.test.ts @@ -32,7 +32,7 @@ test('required by default', () => { context_value_1: 0, context_value_2: 0, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"expected value of type [string] but got [undefined]"`); }); test('returns default', () => { @@ -90,7 +90,9 @@ test('properly validates types according chosen schema', () => { context_value_1: 0, context_value_2: 0, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [1] but it must have a minimum length of [2]."` + ); expect( type.validate('ab', { @@ -104,7 +106,9 @@ test('properly validates types according chosen schema', () => { context_value_1: 0, context_value_2: 1, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [2] but it must have a maximum length of [1]."` + ); expect( type.validate('a', { @@ -126,7 +130,9 @@ test('properly validates when compares with Schema', () => { type.validate('a', { context_value_1: 0, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [1] but it must have a minimum length of [2]."` + ); expect( type.validate('ab', { @@ -138,7 +144,9 @@ test('properly validates when compares with Schema', () => { type.validate('ab', { context_value_1: 'b', }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [2] but it must have a minimum length of [3]."` + ); expect( type.validate('abc', { @@ -159,7 +167,9 @@ test('properly validates when compares with "null" literal Schema', () => { type.validate('a', { context_value_1: null, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [1] but it must have a minimum length of [2]."` + ); expect( type.validate('ab', { @@ -171,7 +181,9 @@ test('properly validates when compares with "null" literal Schema', () => { type.validate('ab', { context_value_1: 'b', }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [2] but it must have a minimum length of [3]."` + ); expect( type.validate('abc', { @@ -193,7 +205,7 @@ test('properly handles schemas with incompatible types', () => { context_value_1: 0, context_value_2: 0, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"expected value of type [string] but got [boolean]"`); expect( type.validate('a', { @@ -207,7 +219,7 @@ test('properly handles schemas with incompatible types', () => { context_value_1: 0, context_value_2: 1, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"expected value of type [boolean] but got [string]"`); expect( type.validate(true, { @@ -223,14 +235,18 @@ test('properly handles conditionals within objects', () => { value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), }); - expect(() => type.validate({ key: 'string', value: 1 })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ key: 'string', value: 1 })).toThrowErrorMatchingInlineSnapshot( + `"[value]: expected value of type [string] but got [number]"` + ); expect(type.validate({ key: 'string', value: 'a' })).toEqual({ key: 'string', value: 'a', }); - expect(() => type.validate({ key: 'number', value: 'a' })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ key: 'number', value: 'a' })).toThrowErrorMatchingInlineSnapshot( + `"[value]: expected value of type [number] but got [string]"` + ); expect(type.validate({ key: 'number', value: 1 })).toEqual({ key: 'number', @@ -269,7 +285,9 @@ test('works with both context and sibling references', () => { expect(() => type.validate({ key: 'string', value: 1 }, { context_key: 'number' }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[value]: expected value of type [string] but got [number]"` + ); expect(type.validate({ key: 'string', value: 'a' }, { context_key: 'number' })).toEqual({ key: 'string', @@ -278,7 +296,9 @@ test('works with both context and sibling references', () => { expect(() => type.validate({ key: 'number', value: 'a' }, { context_key: 'number' }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[value]: expected value of type [number] but got [string]"` + ); expect(type.validate({ key: 'number', value: 1 }, { context_key: 'number' })).toEqual({ key: 'number', @@ -294,11 +314,15 @@ test('includes namespace into failures', () => { expect(() => type.validate({ key: 'string', value: 1 }, {}, 'mega-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[mega-namespace.value]: expected value of type [string] but got [number]"` + ); expect(() => type.validate({ key: 'number', value: 'a' }, {}, 'mega-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[mega-namespace.value]: expected value of type [number] but got [string]"` + ); }); test('correctly handles missing references', () => { @@ -311,7 +335,9 @@ test('correctly handles missing references', () => { ), }); - expect(() => type.validate({ value: 1 })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ value: 1 })).toThrowErrorMatchingInlineSnapshot( + `"[value]: expected value of type [string] but got [number]"` + ); expect(type.validate({ value: 'a' })).toEqual({ value: 'a' }); }); @@ -332,8 +358,16 @@ test('works within `oneOf`', () => { expect(type.validate(true, { type: 'boolean' })).toEqual(true); expect(type.validate(['a', 'b'], { type: 'array' })).toEqual(['a', 'b']); - expect(() => type.validate(1, { type: 'string' })).toThrowErrorMatchingSnapshot(); - expect(() => type.validate(true, { type: 'string' })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(1, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [number] +- [1]: expected value of type [array] but got [number]" +`); + expect(() => type.validate(true, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [boolean] +- [1]: expected value of type [array] but got [boolean]" +`); }); describe('#validate', () => { diff --git a/packages/kbn-config-schema/src/types/duration_type.test.ts b/packages/kbn-config-schema/src/types/duration_type.test.ts index 57e917dc99b2b..2a0458f1419cc 100644 --- a/packages/kbn-config-schema/src/types/duration_type.test.ts +++ b/packages/kbn-config-schema/src/types/duration_type.test.ts @@ -35,11 +35,17 @@ test('handles number', () => { }); test('is required by default', () => { - expect(() => duration().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [moment.Duration] but got [undefined]"` + ); }); test('includes namespace in failure', () => { - expect(() => duration().validate(undefined, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => + duration().validate(undefined, {}, 'foo-namespace') + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [moment.Duration] but got [undefined]"` + ); }); describe('#defaultValue', () => { @@ -48,7 +54,7 @@ describe('#defaultValue', () => { duration({ defaultValue: momentDuration(1, 'hour'), }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(`"PT1H"`); }); test('can be a string', () => { @@ -56,7 +62,7 @@ describe('#defaultValue', () => { duration({ defaultValue: '1h', }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(`"PT1H"`); }); test('can be a string-formatted number', () => { @@ -64,7 +70,7 @@ describe('#defaultValue', () => { duration({ defaultValue: '600', }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(`"PT0.6S"`); }); test('can be a number', () => { @@ -72,7 +78,7 @@ describe('#defaultValue', () => { duration({ defaultValue: 600, }).validate(undefined) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(`"PT0.6S"`); }); test('can be a function that returns compatible type', () => { @@ -103,12 +109,12 @@ describe('#defaultValue', () => { fromContext: duration({ defaultValue: contextRef('val') }), }).validate({}, { val: momentDuration(700, 'ms') }) ).toMatchInlineSnapshot(` -Object { - "fromContext": "PT0.7S", - "source": "PT0.6S", - "target": "PT0.6S", -} -`); + Object { + "fromContext": "PT0.7S", + "source": "PT0.6S", + "target": "PT0.6S", + } + `); expect( object({ @@ -117,12 +123,12 @@ Object { fromContext: duration({ defaultValue: contextRef('val') }), }).validate({}, { val: momentDuration(2, 'hour') }) ).toMatchInlineSnapshot(` -Object { - "fromContext": "PT2H", - "source": "PT1H", - "target": "PT1H", -} -`); + Object { + "fromContext": "PT2H", + "source": "PT1H", + "target": "PT1H", + } + `); expect( object({ @@ -131,29 +137,45 @@ Object { fromContext: duration({ defaultValue: contextRef('val') }), }).validate({}, { val: momentDuration(2, 'hour') }) ).toMatchInlineSnapshot(` -Object { - "fromContext": "PT2H", - "source": "PT1H", - "target": "PT1H", -} -`); + Object { + "fromContext": "PT2H", + "source": "PT1H", + "target": "PT1H", + } + `); }); }); test('returns error when not valid string or non-safe positive integer', () => { - expect(() => duration().validate(-123)).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(-123)).toThrowErrorMatchingInlineSnapshot( + `"Value in milliseconds is expected to be a safe positive integer."` + ); - expect(() => duration().validate(NaN)).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(NaN)).toThrowErrorMatchingInlineSnapshot( + `"Value in milliseconds is expected to be a safe positive integer."` + ); - expect(() => duration().validate(Infinity)).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(Infinity)).toThrowErrorMatchingInlineSnapshot( + `"Value in milliseconds is expected to be a safe positive integer."` + ); - expect(() => duration().validate(Math.pow(2, 53))).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(Math.pow(2, 53))).toThrowErrorMatchingInlineSnapshot( + `"Value in milliseconds is expected to be a safe positive integer."` + ); - expect(() => duration().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [moment.Duration] but got [Array]"` + ); - expect(() => duration().validate(/abc/)).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [moment.Duration] but got [RegExp]"` + ); - expect(() => duration().validate('123foo')).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate('123foo')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse value as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."` + ); - expect(() => duration().validate('123 456')).toThrowErrorMatchingSnapshot(); + expect(() => duration().validate('123 456')).toThrowErrorMatchingInlineSnapshot( + `"Failed to parse value as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."` + ); }); diff --git a/packages/kbn-config-schema/src/types/literal_type.test.ts b/packages/kbn-config-schema/src/types/literal_type.test.ts index a5ddff3152368..abcf5bb2a3b2d 100644 --- a/packages/kbn-config-schema/src/types/literal_type.test.ts +++ b/packages/kbn-config-schema/src/types/literal_type.test.ts @@ -38,17 +38,29 @@ test('handles null', () => { }); test('returns error when not correct', () => { - expect(() => literal('test').validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => literal('test').validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value to equal [test]"` + ); - expect(() => literal(true).validate(false)).toThrowErrorMatchingSnapshot(); + expect(() => literal(true).validate(false)).toThrowErrorMatchingInlineSnapshot( + `"expected value to equal [true]"` + ); - expect(() => literal('test').validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => literal('test').validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value to equal [test]"` + ); - expect(() => literal(123).validate('abc')).toThrowErrorMatchingSnapshot(); + expect(() => literal(123).validate('abc')).toThrowErrorMatchingInlineSnapshot( + `"expected value to equal [123]"` + ); - expect(() => literal(null).validate(42)).toThrowErrorMatchingSnapshot(); + expect(() => literal(null).validate(42)).toThrowErrorMatchingInlineSnapshot( + `"expected value to equal [null]"` + ); }); test('includes namespace in failure', () => { - expect(() => literal('test').validate('foo', {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => + literal('test').validate('foo', {}, 'foo-namespace') + ).toThrowErrorMatchingInlineSnapshot(`"[foo-namespace]: expected value to equal [test]"`); }); diff --git a/packages/kbn-config-schema/src/types/literal_type.ts b/packages/kbn-config-schema/src/types/literal_type.ts index b5ddaa2f68d4f..5ba0b417683bd 100644 --- a/packages/kbn-config-schema/src/types/literal_type.ts +++ b/packages/kbn-config-schema/src/types/literal_type.ts @@ -29,7 +29,7 @@ export class LiteralType extends Type { switch (type) { case 'any.required': case 'any.allowOnly': - return `expected value to equal [${expectedValue}] but got [${value}]`; + return `expected value to equal [${expectedValue}]`; } } } diff --git a/packages/kbn-config-schema/src/types/map_of_type.test.ts b/packages/kbn-config-schema/src/types/map_of_type.test.ts index 3cb3d2d0b6862..b015f51bdc8ad 100644 --- a/packages/kbn-config-schema/src/types/map_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/map_of_type.test.ts @@ -40,7 +40,7 @@ test('properly parse the value if input is a string', () => { test('fails if string input cannot be parsed', () => { const type = schema.mapOf(schema.string(), schema.string()); expect(() => type.validate(`invalidjson`)).toThrowErrorMatchingInlineSnapshot( - `"could not parse map value from [invalidjson]"` + `"could not parse map value from json input"` ); }); @@ -169,7 +169,7 @@ test('error preserves full path', () => { expect(() => type.validate({ grandParentKey: { parentKey: { a: 'some-value' } } }) ).toThrowErrorMatchingInlineSnapshot( - `"[grandParentKey.parentKey.key(\\"a\\")]: value is [a] but it must have a minimum length of [2]."` + `"[grandParentKey.parentKey.key(\\"a\\")]: value has length [1] but it must have a minimum length of [2]."` ); expect(() => diff --git a/packages/kbn-config-schema/src/types/map_type.ts b/packages/kbn-config-schema/src/types/map_type.ts index 1c0c473f98ec1..231c3726ae9d5 100644 --- a/packages/kbn-config-schema/src/types/map_type.ts +++ b/packages/kbn-config-schema/src/types/map_type.ts @@ -49,7 +49,7 @@ export class MapOfType extends Type> { case 'map.base': return `expected value of type [Map] or [object] but got [${typeDetect(value)}]`; case 'map.parse': - return `could not parse map value from [${value}]`; + return `could not parse map value from json input`; case 'map.key': case 'map.value': const childPathWithIndex = path.slice(); diff --git a/packages/kbn-config-schema/src/types/maybe_type.test.ts b/packages/kbn-config-schema/src/types/maybe_type.test.ts index c35fa18593520..2a1278f5e801c 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.test.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.test.ts @@ -42,23 +42,31 @@ test('returns undefined even if contained type has a default value', () => { test('validates contained type', () => { const type = schema.maybe(schema.string({ maxLength: 1 })); - expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"value has length [3] but it must have a maximum length of [1]."` + ); }); test('validates basic type', () => { const type = schema.maybe(schema.string()); - expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(666)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); }); test('fails if null', () => { const type = schema.maybe(schema.string()); - expect(() => type.validate(null)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(null)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [null]"` + ); }); test('includes namespace in failure', () => { const type = schema.maybe(schema.string()); - expect(() => type.validate(null, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(null, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [string] but got [null]"` + ); }); describe('maybe + object', () => { diff --git a/packages/kbn-config-schema/src/types/never_type.test.ts b/packages/kbn-config-schema/src/types/never_type.test.ts index 46f0b47f56ad6..2e5bc0e8c672d 100644 --- a/packages/kbn-config-schema/src/types/never_type.test.ts +++ b/packages/kbn-config-schema/src/types/never_type.test.ts @@ -22,10 +22,18 @@ import { schema } from '..'; test('throws on any value set', () => { const type = schema.never(); - expect(() => type.validate(1)).toThrowErrorMatchingSnapshot(); - expect(() => type.validate('a')).toThrowErrorMatchingSnapshot(); - expect(() => type.validate(null)).toThrowErrorMatchingSnapshot(); - expect(() => type.validate({})).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(1)).toThrowErrorMatchingInlineSnapshot( + `"a value wasn't expected to be present"` + ); + expect(() => type.validate('a')).toThrowErrorMatchingInlineSnapshot( + `"a value wasn't expected to be present"` + ); + expect(() => type.validate(null)).toThrowErrorMatchingInlineSnapshot( + `"a value wasn't expected to be present"` + ); + expect(() => type.validate({})).toThrowErrorMatchingInlineSnapshot( + `"a value wasn't expected to be present"` + ); expect(() => type.validate(undefined)).not.toThrow(); }); @@ -37,7 +45,7 @@ test('throws on value set as object property', () => { expect(() => type.validate({ name: 'name', status: 'in progress' }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"[name]: a value wasn't expected to be present"`); expect(() => type.validate({ status: 'in progress' })).not.toThrow(); expect(() => type.validate({ name: undefined, status: 'in progress' })).not.toThrow(); @@ -71,5 +79,5 @@ test('works for conditional types', () => { context_value_2: 1, } ) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"[name]: a value wasn't expected to be present"`); }); diff --git a/packages/kbn-config-schema/src/types/nullable_type.test.ts b/packages/kbn-config-schema/src/types/nullable_type.test.ts index ed04202950a2c..fb9d544a3eb0e 100644 --- a/packages/kbn-config-schema/src/types/nullable_type.test.ts +++ b/packages/kbn-config-schema/src/types/nullable_type.test.ts @@ -94,13 +94,21 @@ test('returns null even if contained type has a default value', () => { test('validates contained type', () => { const type = schema.nullable(schema.string({ maxLength: 1 })); - expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('foo')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: value has length [3] but it must have a maximum length of [1]. +- [1]: expected value to equal [null]" +`); }); test('validates basic type', () => { const type = schema.nullable(schema.string()); - expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(666)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [number] +- [1]: expected value to equal [null]" +`); }); test('validates type in object', () => { @@ -121,11 +129,19 @@ test('validates type errors in object', () => { bar: schema.nullable(schema.boolean()), }); - expect(() => type.validate({ foo: 'ab' })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ foo: 'ab' })).toThrowErrorMatchingInlineSnapshot(` +"[foo]: types that failed validation: +- [foo.0]: value has length [2] but it must have a maximum length of [1]. +- [foo.1]: expected value to equal [null]" +`); }); test('includes namespace in failure', () => { const type = schema.nullable(schema.string({ maxLength: 1 })); - expect(() => type.validate('foo', {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('foo', {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(` +"[foo-namespace]: types that failed validation: +- [foo-namespace.0]: value has length [3] but it must have a maximum length of [1]. +- [foo-namespace.1]: expected value to equal [null]" +`); }); diff --git a/packages/kbn-config-schema/src/types/number_type.test.ts b/packages/kbn-config-schema/src/types/number_type.test.ts index b85d5113563eb..cfcb0e99afbd5 100644 --- a/packages/kbn-config-schema/src/types/number_type.test.ts +++ b/packages/kbn-config-schema/src/types/number_type.test.ts @@ -32,17 +32,23 @@ test('handles numeric strings with floats', () => { }); test('fails if number is `NaN`', () => { - expect(() => schema.number().validate(NaN)).toThrowErrorMatchingSnapshot(); + expect(() => schema.number().validate(NaN)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [number]"` + ); }); test('is required by default', () => { - expect(() => schema.number().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.number().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.number().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [number] but got [undefined]"` + ); }); describe('#min', () => { @@ -51,7 +57,9 @@ describe('#min', () => { }); test('returns error when smaller number', () => { - expect(() => schema.number({ min: 4 }).validate(3)).toThrowErrorMatchingSnapshot(); + expect(() => schema.number({ min: 4 }).validate(3)).toThrowErrorMatchingInlineSnapshot( + `"Value must be equal to or greater than [4]."` + ); }); }); @@ -61,7 +69,9 @@ describe('#max', () => { }); test('returns error when larger number', () => { - expect(() => schema.number({ max: 2 }).validate(3)).toThrowErrorMatchingSnapshot(); + expect(() => schema.number({ max: 2 }).validate(3)).toThrowErrorMatchingInlineSnapshot( + `"Value must be equal to or lower than [2]."` + ); }); }); @@ -76,9 +86,15 @@ describe('#defaultValue', () => { }); test('returns error when not number or numeric string', () => { - expect(() => schema.number().validate('test')).toThrowErrorMatchingSnapshot(); + expect(() => schema.number().validate('test')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [string]"` + ); - expect(() => schema.number().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.number().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [Array]"` + ); - expect(() => schema.number().validate(/abc/)).toThrowErrorMatchingSnapshot(); + expect(() => schema.number().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [number] but got [RegExp]"` + ); }); diff --git a/packages/kbn-config-schema/src/types/number_type.ts b/packages/kbn-config-schema/src/types/number_type.ts index ada4d1909c917..81ca4449122a3 100644 --- a/packages/kbn-config-schema/src/types/number_type.ts +++ b/packages/kbn-config-schema/src/types/number_type.ts @@ -46,9 +46,9 @@ export class NumberType extends Type { case 'number.base': return `expected value of type [number] but got [${typeDetect(value)}]`; case 'number.min': - return `Value is [${value}] but it must be equal to or greater than [${limit}].`; + return `Value must be equal to or greater than [${limit}].`; case 'number.max': - return `Value is [${value}] but it must be equal to or lower than [${limit}].`; + return `Value must be equal to or lower than [${limit}].`; } } } diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 64739d7a4c4da..29e341983fde9 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -49,7 +49,7 @@ test('fails if string input cannot be parsed', () => { name: schema.string(), }); expect(() => type.validate(`invalidjson`)).toThrowErrorMatchingInlineSnapshot( - `"could not parse object value from [invalidjson]"` + `"could not parse object value from json input"` ); }); @@ -181,7 +181,7 @@ test('called with wrong type', () => { const type = schema.object({}); expect(() => type.validate('foo')).toThrowErrorMatchingInlineSnapshot( - `"could not parse object value from [foo]"` + `"could not parse object value from json input"` ); expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot( `"expected a plain object value, but found [number] instead."` diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 4f3d68a6bac97..f34acd0d2ce65 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -62,7 +62,7 @@ export class ObjectType

extends Type> case 'object.base': return `expected a plain object value, but found [${typeDetect(value)}] instead.`; case 'object.parse': - return `could not parse object value from [${value}]`; + return `could not parse object value from json input`; case 'object.allowUnknown': return `definition for this key is missing`; case 'object.child': diff --git a/packages/kbn-config-schema/src/types/one_of_type.test.ts b/packages/kbn-config-schema/src/types/one_of_type.test.ts index c9da1a6cd8494..deb87a485cdfe 100644 --- a/packages/kbn-config-schema/src/types/one_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/one_of_type.test.ts @@ -75,13 +75,20 @@ test('handles object', () => { test('handles object with wrong type', () => { const type = schema.oneOf([schema.object({ age: schema.number() })]); - expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0.age]: expected value of type [number] but got [string]" +`); }); test('includes namespace in failure', () => { const type = schema.oneOf([schema.object({ age: schema.number() })]); - expect(() => type.validate({ age: 'foo' }, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ age: 'foo' }, {}, 'foo-namespace')) + .toThrowErrorMatchingInlineSnapshot(` +"[foo-namespace]: types that failed validation: +- [foo-namespace.0.age]: expected value of type [number] but got [string]" +`); }); test('handles multiple objects with same key', () => { @@ -110,20 +117,33 @@ test('handles maybe', () => { test('fails if not matching type', () => { const type = schema.oneOf([schema.string()]); - expect(() => type.validate(false)).toThrowErrorMatchingSnapshot(); - expect(() => type.validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [boolean]" +`); + expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [number]" +`); }); test('fails if not matching multiple types', () => { const type = schema.oneOf([schema.string(), schema.number()]); - expect(() => type.validate(false)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value of type [string] but got [boolean] +- [1]: expected value of type [number] but got [boolean]" +`); }); test('fails if not matching literal', () => { const type = schema.oneOf([schema.literal('foo')]); - expect(() => type.validate('bar')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('bar')).toThrowErrorMatchingInlineSnapshot(` +"types that failed validation: +- [0]: expected value to equal [foo]" +`); }); test('fails if nested union type fail', () => { @@ -138,7 +158,7 @@ test('fails if nested union type fail', () => { - [0]: expected value of type [boolean] but got [string] - [1]: types that failed validation: - [0]: types that failed validation: - - [0]: could not parse object value from [aaa] + - [0]: could not parse object value from json input - [1]: expected value of type [number] but got [string]" `); }); diff --git a/packages/kbn-config-schema/src/types/record_of_type.test.ts b/packages/kbn-config-schema/src/types/record_of_type.test.ts index f3ab1925597b5..ef15e7b0f6ad6 100644 --- a/packages/kbn-config-schema/src/types/record_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/record_of_type.test.ts @@ -73,8 +73,8 @@ test('fails when not receiving expected key type', () => { expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(` "[key(\\"name\\")]: types that failed validation: -- [0]: expected value to equal [nickName] but got [name] -- [1]: expected value to equal [lastName] but got [name]" +- [0]: expected value to equal [nickName] +- [1]: expected value to equal [lastName]" `); }); @@ -88,8 +88,8 @@ test('fails after parsing when not receiving expected key type', () => { expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(` "[key(\\"name\\")]: types that failed validation: -- [0]: expected value to equal [nickName] but got [name] -- [1]: expected value to equal [lastName] but got [name]" +- [0]: expected value to equal [nickName] +- [1]: expected value to equal [lastName]" `); }); @@ -118,7 +118,7 @@ test('includes namespace in failure when wrong key type', () => { }; expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( - `"[foo-namespace.key(\\"name\\")]: value is [name] but it must have a minimum length of [10]."` + `"[foo-namespace.key(\\"name\\")]: value has length [4] but it must have a minimum length of [10]."` ); }); @@ -169,7 +169,7 @@ test('error preserves full path', () => { expect(() => type.validate({ grandParentKey: { parentKey: { a: 'some-value' } } }) ).toThrowErrorMatchingInlineSnapshot( - `"[grandParentKey.parentKey.key(\\"a\\")]: value is [a] but it must have a minimum length of [2]."` + `"[grandParentKey.parentKey.key(\\"a\\")]: value has length [1] but it must have a minimum length of [2]."` ); expect(() => diff --git a/packages/kbn-config-schema/src/types/record_type.ts b/packages/kbn-config-schema/src/types/record_type.ts index b795c83acdadb..c6d4b4d71b4f1 100644 --- a/packages/kbn-config-schema/src/types/record_type.ts +++ b/packages/kbn-config-schema/src/types/record_type.ts @@ -41,7 +41,7 @@ export class RecordOfType extends Type> { case 'record.base': return `expected value of type [object] but got [${typeDetect(value)}]`; case 'record.parse': - return `could not parse record value from [${value}]`; + return `could not parse record value from json input`; case 'record.key': case 'record.value': const childPathWithIndex = path.slice(); diff --git a/packages/kbn-config-schema/src/types/stream_type.test.ts b/packages/kbn-config-schema/src/types/stream_type.test.ts index 011fa6373df33..2e6f31ad09b34 100644 --- a/packages/kbn-config-schema/src/types/stream_type.test.ts +++ b/packages/kbn-config-schema/src/types/stream_type.test.ts @@ -41,13 +41,17 @@ test('Passthrough is valid', () => { }); test('is required by default', () => { - expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Buffer] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.stream().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [Stream] but got [undefined]"` + ); }); describe('#defaultValue', () => { @@ -63,9 +67,15 @@ describe('#defaultValue', () => { }); test('returns error when not a stream', () => { - expect(() => schema.stream().validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => schema.stream().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Stream] but got [number]"` + ); - expect(() => schema.stream().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.stream().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Stream] but got [Array]"` + ); - expect(() => schema.stream().validate('abc')).toThrowErrorMatchingSnapshot(); + expect(() => schema.stream().validate('abc')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Stream] but got [string]"` + ); }); diff --git a/packages/kbn-config-schema/src/types/string_type.test.ts b/packages/kbn-config-schema/src/types/string_type.test.ts index d599ea65c5ae2..c1d853fe82b82 100644 --- a/packages/kbn-config-schema/src/types/string_type.test.ts +++ b/packages/kbn-config-schema/src/types/string_type.test.ts @@ -28,13 +28,17 @@ test('allows empty strings', () => { }); test('is required by default', () => { - expect(() => schema.string().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.string().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [undefined]"` + ); }); test('includes namespace in failure', () => { expect(() => schema.string().validate(undefined, {}, 'foo-namespace') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [string] but got [undefined]"` + ); }); describe('#minLength', () => { @@ -43,11 +47,17 @@ describe('#minLength', () => { }); test('returns error when shorter string', () => { - expect(() => schema.string({ minLength: 4 }).validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => + schema.string({ minLength: 4 }).validate('foo') + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [3] but it must have a minimum length of [4]."` + ); }); test('returns error when empty string', () => { - expect(() => schema.string({ minLength: 2 }).validate('')).toThrowErrorMatchingSnapshot(); + expect(() => schema.string({ minLength: 2 }).validate('')).toThrowErrorMatchingInlineSnapshot( + `"value has length [0] but it must have a minimum length of [2]."` + ); }); }); @@ -57,7 +67,11 @@ describe('#maxLength', () => { }); test('returns error when longer string', () => { - expect(() => schema.string({ maxLength: 2 }).validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => + schema.string({ maxLength: 2 }).validate('foo') + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [3] but it must have a maximum length of [2]."` + ); }); }); @@ -84,23 +98,37 @@ describe('#hostname', () => { test('returns error when value is not a valid hostname', () => { const hostNameSchema = schema.string({ hostname: true }); - expect(() => hostNameSchema.validate('host:name')).toThrowErrorMatchingSnapshot(); - expect(() => hostNameSchema.validate('localhost:5601')).toThrowErrorMatchingSnapshot(); - expect(() => hostNameSchema.validate('-')).toThrowErrorMatchingSnapshot(); - expect(() => hostNameSchema.validate('0:?:0:0:0:0:0:1')).toThrowErrorMatchingSnapshot(); + expect(() => hostNameSchema.validate('host:name')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); + expect(() => hostNameSchema.validate('localhost:5601')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); + expect(() => hostNameSchema.validate('-')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); + expect(() => hostNameSchema.validate('0:?:0:0:0:0:0:1')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); const tooLongHostName = 'a'.repeat(256); - expect(() => hostNameSchema.validate(tooLongHostName)).toThrowErrorMatchingSnapshot(); + expect(() => hostNameSchema.validate(tooLongHostName)).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid hostname (see RFC 1123)."` + ); }); test('returns error when empty string', () => { - expect(() => schema.string({ hostname: true }).validate('')).toThrowErrorMatchingSnapshot(); + expect(() => schema.string({ hostname: true }).validate('')).toThrowErrorMatchingInlineSnapshot( + `"any.empty"` + ); }); test('supports string validation rules', () => { expect(() => schema.string({ hostname: true, maxLength: 3 }).validate('www.example.com') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [15] but it must have a maximum length of [3]."` + ); }); }); @@ -146,20 +174,30 @@ describe('#validate', () => { test('throws when returns string', () => { const validate = () => 'validator failure'; - expect(() => schema.string({ validate }).validate('foo')).toThrowErrorMatchingSnapshot(); + expect(() => schema.string({ validate }).validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"validator failure"` + ); }); test('throw when empty string', () => { const validate = () => 'validator failure'; - expect(() => schema.string({ validate }).validate('')).toThrowErrorMatchingSnapshot(); + expect(() => schema.string({ validate }).validate('')).toThrowErrorMatchingInlineSnapshot( + `"validator failure"` + ); }); }); test('returns error when not string', () => { - expect(() => schema.string().validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => schema.string().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); - expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [Array]"` + ); - expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingSnapshot(); + expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [RegExp]"` + ); }); diff --git a/packages/kbn-config-schema/src/types/string_type.ts b/packages/kbn-config-schema/src/types/string_type.ts index 6d5fa93c0299c..7f49440b8d7e2 100644 --- a/packages/kbn-config-schema/src/types/string_type.ts +++ b/packages/kbn-config-schema/src/types/string_type.ts @@ -45,7 +45,7 @@ export class StringType extends Type { if (options.minLength !== undefined) { schema = schema.custom(value => { if (value.length < options.minLength!) { - return `value is [${value}] but it must have a minimum length of [${options.minLength}].`; + return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`; } }); } @@ -53,7 +53,7 @@ export class StringType extends Type { if (options.maxLength !== undefined) { schema = schema.custom(value => { if (value.length > options.maxLength!) { - return `value is [${value}] but it must have a maximum length of [${options.maxLength}].`; + return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`; } }); } @@ -66,7 +66,7 @@ export class StringType extends Type { case 'any.required': return `expected value of type [string] but got [${typeDetect(value)}]`; case 'string.hostname': - return `value is [${value}] but it must be a valid hostname (see RFC 1123).`; + return `value must be a valid hostname (see RFC 1123).`; } } } diff --git a/packages/kbn-config-schema/src/types/uri_type.test.ts b/packages/kbn-config-schema/src/types/uri_type.test.ts index 1345b47a63c1f..72e5ca6f7171e 100644 --- a/packages/kbn-config-schema/src/types/uri_type.test.ts +++ b/packages/kbn-config-schema/src/types/uri_type.test.ts @@ -20,7 +20,9 @@ import { schema } from '..'; test('is required by default', () => { - expect(() => schema.uri().validate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => schema.uri().validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [undefined]."` + ); }); test('returns value for valid URI as per RFC3986', () => { @@ -54,17 +56,23 @@ test('returns value for valid URI as per RFC3986', () => { test('returns error when value is not a URI', () => { const uriSchema = schema.uri(); - expect(() => uriSchema.validate('3domain.local')).toThrowErrorMatchingSnapshot(); + expect(() => uriSchema.validate('3domain.local')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid URI (see RFC 3986)."` + ); expect(() => uriSchema.validate('http://8010:0:0:0:9:500:300C:200A') - ).toThrowErrorMatchingSnapshot(); - expect(() => uriSchema.validate('-')).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"value must be a valid URI (see RFC 3986)."`); + expect(() => uriSchema.validate('-')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid URI (see RFC 3986)."` + ); expect(() => uriSchema.validate('https://example.com?baz[]=foo&baz[]=bar') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"value must be a valid URI (see RFC 3986)."`); const tooLongUri = `http://${'a'.repeat(256)}`; - expect(() => uriSchema.validate(tooLongUri)).toThrowErrorMatchingSnapshot(); + expect(() => uriSchema.validate(tooLongUri)).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid URI (see RFC 3986)."` + ); }); describe('#scheme', () => { @@ -78,8 +86,12 @@ describe('#scheme', () => { test('returns error when shorter string', () => { const uriSchema = schema.uri({ scheme: ['http', 'https'] }); - expect(() => uriSchema.validate('ftp://elastic.co')).toThrowErrorMatchingSnapshot(); - expect(() => uriSchema.validate('file:///kibana.log')).toThrowErrorMatchingSnapshot(); + expect(() => uriSchema.validate('ftp://elastic.co')).toThrowErrorMatchingInlineSnapshot( + `"expected URI with scheme [http|https]."` + ); + expect(() => uriSchema.validate('file:///kibana.log')).toThrowErrorMatchingInlineSnapshot( + `"expected URI with scheme [http|https]."` + ); }); }); @@ -131,14 +143,20 @@ describe('#validate', () => { expect(() => schema.uri({ validate }).validate('http://kibana.local') - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"validator failure"`); }); }); test('returns error when not string', () => { - expect(() => schema.uri().validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => schema.uri().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]."` + ); - expect(() => schema.uri().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => schema.uri().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [Array]."` + ); - expect(() => schema.uri().validate(/abc/)).toThrowErrorMatchingSnapshot(); + expect(() => schema.uri().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [RegExp]."` + ); }); diff --git a/packages/kbn-config-schema/src/types/uri_type.ts b/packages/kbn-config-schema/src/types/uri_type.ts index df1ce9e869d3b..f365ed35e3579 100644 --- a/packages/kbn-config-schema/src/types/uri_type.ts +++ b/packages/kbn-config-schema/src/types/uri_type.ts @@ -36,9 +36,9 @@ export class URIType extends Type { case 'string.base': return `expected value of type [string] but got [${typeDetect(value)}].`; case 'string.uriCustomScheme': - return `expected URI with scheme [${scheme}] but got [${value}].`; + return `expected URI with scheme [${scheme}].`; case 'string.uri': - return `value is [${value}] but it must be a valid URI (see RFC 3986).`; + return `value must be a valid URI (see RFC 3986).`; } } } diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 72ff9162ffe6c..1531c1d22b01b 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { resolve } = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); // eslint-disable-next-line import/no-unresolved @@ -72,6 +73,38 @@ module.exports = async ({ config }) => { ], }); + // Enable SASS + config.module.rules.push({ + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: resolve(REPO_ROOT, 'src/optimize/'), + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], + }); + // Reference the built DLL file of static(ish) dependencies, which are removed // during kbn:bootstrap and rebuilt if missing. config.plugins.push( @@ -96,7 +129,7 @@ module.exports = async ({ config }) => { ); // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx'); + config.resolve.extensions.push('.ts', '.tsx', '.scss'); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { diff --git a/src/cli_plugin/install/cleanup.js b/src/cli_plugin/install/cleanup.js index fa4bdcf4f6966..eaa25962ef0e4 100644 --- a/src/cli_plugin/install/cleanup.js +++ b/src/cli_plugin/install/cleanup.js @@ -27,7 +27,7 @@ export function cleanPrevious(settings, logger) { logger.log('Found previous install attempt. Deleting...'); try { - del.sync(settings.workingPath); + del.sync(settings.workingPath, { force: true }); } catch (e) { reject(e); } diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 5a341e67dc128..92be2ac250320 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -46,7 +46,7 @@ export default async function install(settings, logger) { await extract(settings, logger); - del.sync(settings.tempArchiveFile); + del.sync(settings.tempArchiveFile, { force: true }); existingInstall(settings, logger); diff --git a/src/cli_plugin/remove/remove.js b/src/cli_plugin/remove/remove.js index 8432d0f44836b..353e592390ff4 100644 --- a/src/cli_plugin/remove/remove.js +++ b/src/cli_plugin/remove/remove.js @@ -37,7 +37,7 @@ export default function remove(settings, logger) { } logger.log(`Removing ${settings.plugin}...`); - del.sync(settings.pluginPath); + del.sync(settings.pluginPath, { force: true }); logger.log('Plugin removal complete'); } catch (err) { logger.error(`Unable to remove plugin because of error: "${err.message}"`); diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 05d718e1073df..d602422c14634 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -37,8 +37,7 @@ export interface CapabilitiesStart { */ export class CapabilitiesService { public async start({ appIds, http }: StartDeps): Promise { - const route = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/defaults' : ''; - const capabilities = await http.post(`/api/core/capabilities${route}`, { + const capabilities = await http.post('/api/core/capabilities', { body: JSON.stringify({ applications: appIds }), }); diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts index aace0b9debf9c..7d2e7391aa8d4 100644 --- a/src/core/server/capabilities/capabilities_service.test.ts +++ b/src/core/server/capabilities/capabilities_service.test.ts @@ -41,8 +41,8 @@ describe('CapabilitiesService', () => { }); it('registers the capabilities routes', async () => { - expect(http.createRouter).toHaveBeenCalledWith('/api/core/capabilities'); - expect(router.post).toHaveBeenCalledTimes(2); + expect(http.createRouter).toHaveBeenCalledWith(''); + expect(router.post).toHaveBeenCalledTimes(1); expect(router.post).toHaveBeenCalledWith(expect.any(Object), expect.any(Function)); }); diff --git a/src/core/server/capabilities/routes/index.ts b/src/core/server/capabilities/routes/index.ts index ccaa4621d7003..74c485986a77b 100644 --- a/src/core/server/capabilities/routes/index.ts +++ b/src/core/server/capabilities/routes/index.ts @@ -22,6 +22,6 @@ import { InternalHttpServiceSetup } from '../../http'; import { registerCapabilitiesRoutes } from './resolve_capabilities'; export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) { - const router = http.createRouter('/api/core/capabilities'); + const router = http.createRouter(''); registerCapabilitiesRoutes(router, resolver); } diff --git a/src/core/server/capabilities/routes/resolve_capabilities.ts b/src/core/server/capabilities/routes/resolve_capabilities.ts index 5e1d49b4b1b7e..3fb1bb3d13d0b 100644 --- a/src/core/server/capabilities/routes/resolve_capabilities.ts +++ b/src/core/server/capabilities/routes/resolve_capabilities.ts @@ -22,30 +22,24 @@ import { IRouter } from '../../http'; import { CapabilitiesResolver } from '../resolve_capabilities'; export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) { - // Capabilities are fetched on both authenticated and anonymous routes. - // However when `authRequired` is false, authentication is not performed - // and only default capabilities are returned (all disabled), even for authenticated users. - // So we need two endpoints to handle both scenarios. - [true, false].forEach(authRequired => { - router.post( - { - path: authRequired ? '' : '/defaults', - options: { - authRequired, - }, - validate: { - body: schema.object({ - applications: schema.arrayOf(schema.string()), - }), - }, + router.post( + { + path: '/api/core/capabilities', + options: { + authRequired: 'optional', }, - async (ctx, req, res) => { - const { applications } = req.body; - const capabilities = await resolver(req, applications); - return res.ok({ - body: capabilities, - }); - } - ); - }); + validate: { + body: schema.object({ + applications: schema.arrayOf(schema.string()), + }), + }, + }, + async (ctx, req, res) => { + const { applications } = req.body; + const capabilities = await resolver(req, applications); + return res.ok({ + body: capabilities, + }); + } + ); } diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 28933a035c870..07c153a7a8a20 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -87,7 +87,7 @@ exports[`throws if basepath is missing prepended slash 1`] = `"[basePath]: must exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`; -exports[`throws if invalid hostname 1`] = `"[host]: value is [asdf$%^] but it must be a valid hostname (see RFC 1123)."`; +exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; @@ -100,7 +100,7 @@ Array [ ] `; -exports[`with compression throws if invalid referrer whitelist 1`] = `"[compression.referrerWhitelist.0]: value is [asdf$%^] but it must be a valid hostname (see RFC 1123)."`; +exports[`with compression throws if invalid referrer whitelist 1`] = `"[compression.referrerWhitelist.0]: value must be a valid hostname (see RFC 1123)."`; exports[`with compression throws if invalid referrer whitelist 2`] = `"[compression.referrerWhitelist]: array size is [0], but cannot be smaller than [1]"`; diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 741c723ca9365..bbef0a105c089 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -36,6 +36,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; interface RequestFixtureOptions

{ + auth?: { isAuthenticated: boolean }; headers?: Record; params?: Record; body?: Record; @@ -65,11 +66,13 @@ function createKibanaRequestMock

({ routeAuthRequired, validation = {}, kibanaRouteState = { xsrfRequired: true }, + auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); return KibanaRequest.from( createRawRequestMock({ + auth, headers, params, query, @@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial = {}) { {}, { app: { xsrfRequired: true } as any, + auth: { + isAuthenticated: true, + }, headers: {}, path: '/', route: { settings: {} }, diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index cffdffab0d0cf..f898ed0ea1a99 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,8 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; - -import { IRouter, KibanaRouteState, isSafeMethod } from './router'; +import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -148,7 +147,7 @@ export class HttpServer { this.log.debug(`registering route handler for [${route.path}]`); // Hapi does not allow payload validation to be specified for 'head' or 'get' requests const validate = isSafeMethod(route.method) ? undefined : { payload: true }; - const { authRequired = true, tags, body = {} } = route.options; + const { authRequired, tags, body = {} } = route.options; const { accepts: allow, maxBytes, output, parse } = body; const kibanaRouteState: KibanaRouteState = { @@ -160,8 +159,7 @@ export class HttpServer { method: route.method, path: route.path, options: { - // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` - auth: authRequired === true ? undefined : false, + auth: this.getAuthOption(authRequired), app: kibanaRouteState, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. @@ -196,6 +194,22 @@ export class HttpServer { this.server = undefined; } + private getAuthOption( + authRequired: RouteConfigOptions['authRequired'] = true + ): undefined | false | { mode: 'required' | 'optional' } { + if (this.authRegistered === false) return undefined; + + if (authRequired === true) { + return { mode: 'required' }; + } + if (authRequired === 'optional') { + return { mode: 'optional' }; + } + if (authRequired === false) { + return false; + } + } + private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) { if (config.basePath === undefined || !config.rewriteBasePath) { return; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 30032ff5da796..442bc93190d86 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -115,6 +115,8 @@ const createOnPostAuthToolkitMock = (): jest.Mocked => ({ const createAuthToolkitMock = (): jest.Mocked => ({ authenticated: jest.fn(), + notHandled: jest.fn(), + redirected: jest.fn(), }); const createOnPreResponseToolkitMock = (): jest.Mocked => ({ diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 8f4c02680f8a3..a75eb04fa0120 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -67,9 +67,12 @@ export { AuthenticationHandler, AuthHeaders, AuthResultParams, + AuthRedirected, + AuthRedirectedParams, AuthToolkit, AuthResult, Authenticated, + AuthNotHandled, AuthResultType, } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 425d8cac1893e..7b1630a7de0be 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -50,7 +50,7 @@ describe('http service', () => { await root.shutdown(); }); describe('#isAuthenticated()', () => { - it('returns true if has been authorized', async () => { + it('returns true if has been authenticated', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; @@ -65,11 +65,11 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); }); - it('returns false if has not been authorized', async () => { + it('returns false if has not been authenticated', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; - await registerAuth((req, res, toolkit) => toolkit.authenticated()); + registerAuth((req, res, toolkit) => toolkit.authenticated()); const router = createRouter(''); router.get( @@ -81,7 +81,7 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); - it('returns false if no authorization mechanism has been registered', async () => { + it('returns false if no authentication mechanism has been registered', async () => { const { http } = await root.setup(); const { createRouter, auth } = http; @@ -94,6 +94,37 @@ describe('http service', () => { await root.start(); await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); + + it('returns true if authenticated on a route with "optional" auth', async () => { + const { http } = await root.setup(); + const { createRouter, auth, registerAuth } = http; + + registerAuth((req, res, toolkit) => toolkit.authenticated()); + const router = createRouter(''); + router.get( + { path: '/is-auth', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) + ); + + await root.start(); + await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); + }); + + it('returns false if not authenticated on a route with "optional" auth', async () => { + const { http } = await root.setup(); + const { createRouter, auth, registerAuth } = http; + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + + const router = createRouter(''); + router.get( + { path: '/is-auth', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) + ); + + await root.start(); + await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); + }); }); describe('#get()', () => { it('returns authenticated status and allow associate auth state with request', async () => { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 6dc7ece1359df..0f0d54e88daca 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -57,7 +57,7 @@ interface StorageData { } describe('OnPreAuth', () => { - it('supports registering request inceptors', async () => { + it('supports registering a request interceptor', async () => { const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -415,6 +415,23 @@ describe('Auth', () => { .expect(200, { content: 'ok' }); }); + it('blocks access to a resource if credentials are not provided', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ body: { content: 'ok' } }) + ); + registerAuth((req, res, t) => t.notHandled()); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('Unauthorized'); + }); + it('enables auth for a route by default if registerAuth has been called', async () => { const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -492,11 +509,9 @@ describe('Auth', () => { router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); const redirectTo = '/redirect-url'; - registerAuth((req, res) => - res.redirected({ - headers: { - location: redirectTo, - }, + registerAuth((req, res, t) => + t.redirected({ + location: redirectTo, }) ); await server.start(); @@ -507,6 +522,19 @@ describe('Auth', () => { expect(response.header.location).toBe(redirectTo); }); + it('throws if redirection url is not provided', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); + registerAuth((req, res, t) => t.redirected({} as any)); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + }); + it(`doesn't expose internal error details`, async () => { const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -865,7 +893,7 @@ describe('Auth', () => { ] `); }); - // eslint-disable-next-line + it(`doesn't share request object between interceptors`, async () => { const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index bc1bbc881315a..85270174fbc04 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -45,6 +45,89 @@ afterEach(async () => { const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); describe('KibanaRequest', () => { + describe('auth', () => { + describe('isAuthenticated', () => { + it('returns false if no auth interceptor was registered', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns false if not authenticated on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.notHandled()); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns false if redirected on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.redirected({ location: '/any' })); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns true if authenticated on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.authenticated()); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: true, + }); + }); + it('returns true if authenticated', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.authenticated()); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: true, + }); + }); + }); + }); describe('events', () => { describe('aborted$', () => { it('emits once and completes when request aborted', async done => { diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index a1523781010d4..ee5b0c50acafb 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -46,6 +46,286 @@ afterEach(async () => { await server.stop(); }); +describe('Options', () => { + describe('authRequired', () => { + describe('optional', () => { + it('User has access to a route if auth mechanism not registered', async () => { + const { server: innerServer, createRouter, auth } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('Authenticated user has access to a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated(); + }); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: true, + requestIsAuthenticated: true, + }); + }); + + it('User with no credentials can access a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('User with invalid credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => res.unauthorized()); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('does not redirect user and allows access to a resource', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => + toolkit.redirected({ + location: '/redirect-to', + }) + ); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + }); + + describe('true', () => { + it('User has access to a route if auth interceptor is not registered', async () => { + const { server: innerServer, createRouter, auth } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('Authenticated user has access to a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated(); + }); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: true, + requestIsAuthenticated: true, + }); + }); + + it('User with no credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('User with invalid credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => res.unauthorized()); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('allows redirecting an user', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + const redirectUrl = '/redirect-to'; + + registerAuth((req, res, toolkit) => + toolkit.redirected({ + location: redirectUrl, + }) + ); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(302); + + expect(result.header.location).toBe(redirectUrl); + }); + }); + + describe('false', () => { + it('does not try to authenticate a user', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const authHook = jest.fn(); + registerAuth(authHook); + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + + expect(authHook).toHaveBeenCalledTimes(0); + }); + }); + }); +}); + describe('Handler', () => { it("Doesn't expose error details if handler throws", async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 036ab0211c2ff..2eaf7e0f6fbfe 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -25,11 +25,14 @@ import { lifecycleResponseFactory, LifecycleResponseFactory, isKibanaResponse, + ResponseHeaders, } from '../router'; /** @public */ export enum AuthResultType { authenticated = 'authenticated', + notHandled = 'notHandled', + redirected = 'redirected', } /** @public */ @@ -38,10 +41,20 @@ export interface Authenticated extends AuthResultParams { } /** @public */ -export type AuthResult = Authenticated; +export interface AuthNotHandled { + type: AuthResultType.notHandled; +} + +/** @public */ +export interface AuthRedirected extends AuthRedirectedParams { + type: AuthResultType.redirected; +} + +/** @public */ +export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; const authResult = { - authenticated(data: Partial = {}): AuthResult { + authenticated(data: AuthResultParams = {}): AuthResult { return { type: AuthResultType.authenticated, state: data.state, @@ -49,8 +62,25 @@ const authResult = { responseHeaders: data.responseHeaders, }; }, + notHandled(): AuthResult { + return { + type: AuthResultType.notHandled, + }; + }, + redirected(headers: { location: string } & ResponseHeaders): AuthResult { + return { + type: AuthResultType.redirected, + headers, + }; + }, isAuthenticated(result: AuthResult): result is Authenticated { - return result && result.type === AuthResultType.authenticated; + return result?.type === AuthResultType.authenticated; + }, + isNotHandled(result: AuthResult): result is AuthNotHandled { + return result?.type === AuthResultType.notHandled; + }, + isRedirected(result: AuthResult): result is AuthRedirected { + return result?.type === AuthResultType.redirected; }, }; @@ -62,7 +92,7 @@ const authResult = { export type AuthHeaders = Record; /** - * Result of an incoming request authentication. + * Result of successful authentication. * @public */ export interface AuthResultParams { @@ -82,6 +112,18 @@ export interface AuthResultParams { responseHeaders?: AuthHeaders; } +/** + * Result of auth redirection. + * @public + */ +export interface AuthRedirectedParams { + /** + * Headers to attach for auth redirect. + * Must include "location" header + */ + headers: { location: string } & ResponseHeaders; +} + /** * @public * A tool set defining an outcome of Auth interceptor for incoming request. @@ -89,10 +131,23 @@ export interface AuthResultParams { export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ authenticated: (data?: AuthResultParams) => AuthResult; + /** + * User has no credentials. + * Allows user to access a resource when authRequired: 'optional' + * Rejects a request when authRequired: true + * */ + notHandled: () => AuthResult; + /** + * Redirects user to another location to complete authentication when authRequired: true + * Allows user to access a resource without redirection when authRequired: 'optional' + * */ + redirected: (headers: { location: string } & ResponseHeaders) => AuthResult; } const toolkit: AuthToolkit = { authenticated: authResult.authenticated, + notHandled: authResult.notHandled, + redirected: authResult.redirected, }; /** @@ -109,30 +164,51 @@ export type AuthenticationHandler = ( export function adoptToHapiAuthFormat( fn: AuthenticationHandler, log: Logger, - onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined + onAuth: (request: Request, data: AuthResultParams) => void = () => undefined ) { return async function interceptAuth( request: Request, responseToolkit: ResponseToolkit ): Promise { const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); + const kibanaRequest = KibanaRequest.from(request, undefined, false); + try { - const result = await fn( - KibanaRequest.from(request, undefined, false), - lifecycleResponseFactory, - toolkit - ); + const result = await fn(kibanaRequest, lifecycleResponseFactory, toolkit); + if (isKibanaResponse(result)) { return hapiResponseAdapter.handle(result); } + if (authResult.isAuthenticated(result)) { - onSuccess(request, { + onAuth(request, { state: result.state, requestHeaders: result.requestHeaders, responseHeaders: result.responseHeaders, }); return responseToolkit.authenticated({ credentials: result.state || {} }); } + + if (authResult.isRedirected(result)) { + // we cannot redirect a user when resources with optional auth requested + if (kibanaRequest.route.options.authRequired === 'optional') { + return responseToolkit.continue; + } + + return hapiResponseAdapter.handle( + lifecycleResponseFactory.redirected({ + // hapi doesn't accept string[] as a valid header + headers: result.headers as any, + }) + ); + } + + if (authResult.isNotHandled(result)) { + if (kibanaRequest.route.options.authRequired === 'optional') { + return responseToolkit.continue; + } + return hapiResponseAdapter.handle(lifecycleResponseFactory.unauthorized()); + } throw new Error( `Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.` ); diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index 032027c234485..fb999dc60e39c 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { RouteOptions } from 'hapi'; import { KibanaRequest } from './request'; import { httpServerMock } from '../http_server.mocks'; import { schema } from '@kbn/config-schema'; @@ -117,6 +118,106 @@ describe('KibanaRequest', () => { }); }); + describe('route.options.authRequired property', () => { + it('handles required auth: undefined', () => { + const auth: RouteOptions['auth'] = undefined; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(true); + }); + it('handles required auth: false', () => { + const auth: RouteOptions['auth'] = false; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(false); + }); + it('handles required auth: { mode: "required" }', () => { + const auth: RouteOptions['auth'] = { mode: 'required' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(true); + }); + + it('handles required auth: { mode: "optional" }', () => { + const auth: RouteOptions['auth'] = { mode: 'optional' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe('optional'); + }); + + it('handles required auth: { mode: "try" } as "optional"', () => { + const auth: RouteOptions['auth'] = { mode: 'try' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe('optional'); + }); + + it('throws on auth: strategy name', () => { + const auth: RouteOptions['auth'] = 'session'; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + + expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot( + `"unexpected authentication options: \\"session\\" for route: /"` + ); + }); + + it('throws on auth: { mode: unexpected mode }', () => { + const auth: RouteOptions['auth'] = { mode: undefined }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + + expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot( + `"unexpected authentication options: {} for route: /"` + ); + }); + }); + describe('RouteSchema type inferring', () => { it('should work with config-schema', () => { const body = Buffer.from('body!'); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index bb2db6367f701..f266677c1a172 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -143,6 +143,10 @@ export class KibanaRequest< public readonly socket: IKibanaSocket; /** Request events {@link KibanaRequestEvents} */ public readonly events: KibanaRequestEvents; + public readonly auth: { + /* true if the request has been successfully authenticated, otherwise false. */ + isAuthenticated: boolean; + }; /** @internal */ protected readonly [requestSymbol]: Request; @@ -172,6 +176,11 @@ export class KibanaRequest< this.route = deepFreeze(this.getRouteInfo(request)); this.socket = new KibanaSocket(request.raw.req.socket); this.events = this.getEvents(request); + + this.auth = { + // missing in fakeRequests, so we cast to false + isAuthenticated: Boolean(request.auth?.isAuthenticated), + }; } private getEvents(request: Request): KibanaRequestEvents { @@ -189,7 +198,7 @@ export class KibanaRequest< const { parse, maxBytes, allow, output } = request.route.settings.payload || {}; const options = ({ - authRequired: request.route.settings.auth !== false, + authRequired: this.getAuthRequired(request), // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8 xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true, tags: request.route.settings.tags || [], @@ -209,6 +218,31 @@ export class KibanaRequest< options, }; } + + private getAuthRequired(request: Request): boolean | 'optional' { + const authOptions = request.route.settings.auth; + if (typeof authOptions === 'object') { + // 'try' is used in the legacy platform + if (authOptions.mode === 'optional' || authOptions.mode === 'try') { + return 'optional'; + } + if (authOptions.mode === 'required') { + return true; + } + } + + // legacy platform routes + if (authOptions === undefined) { + return true; + } + + if (authOptions === false) return false; + throw new Error( + `unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${ + this.url.href + }` + ); + } } /** diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index d1458ef4ad063..bb0a8616e7222 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -116,13 +116,15 @@ export interface RouteConfigOptionsBody { */ export interface RouteConfigOptions { /** - * A flag shows that authentication for a route: - * `enabled` when true - * `disabled` when false + * Defines authentication mode for a route: + * - true. A user has to have valid credentials to access a resource + * - false. A user can access a resource without any credentials. + * - 'optional'. A user can access a resource if has valid credentials or no credentials at all. + * Can be useful when we grant access to a resource but want to identify a user if possible. * - * Enabled by default. + * Defaults to `true` if an auth mechanism is registered. */ - authRequired?: boolean; + authRequired?: boolean | 'optional'; /** * Defines xsrf protection requirements for a route: diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts index 738f86f7a69eb..3980b9c247fa3 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/src/core/server/http/ssl_config.test.ts @@ -293,16 +293,16 @@ describe('#sslSchema', () => { expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(` "[supportedProtocols.0]: types that failed validation: -- [supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +- [supportedProtocols.0.0]: expected value to equal [TLSv1] +- [supportedProtocols.0.1]: expected value to equal [TLSv1.1] +- [supportedProtocols.0.2]: expected value to equal [TLSv1.2]" `); expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols)) .toThrowErrorMatchingInlineSnapshot(` "[supportedProtocols.3]: types that failed validation: -- [supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +- [supportedProtocols.3.0]: expected value to equal [TLSv1] +- [supportedProtocols.3.1]: expected value to equal [TLSv1.1] +- [supportedProtocols.3.2]: expected value to equal [TLSv1.2]" `); }); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6967de006be58..e2faf49ba7a9e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -104,9 +104,12 @@ export { AuthResultParams, AuthStatus, AuthToolkit, + AuthRedirected, + AuthRedirectedParams, AuthResult, AuthResultType, Authenticated, + AuthNotHandled, BasePath, IBasePath, CustomHttpResponseOptions, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5aa7520d54446..5ede98a1e6e6d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -419,7 +419,26 @@ export type AuthenticationHandler = (request: KibanaRequest, response: Lifecycle export type AuthHeaders = Record; // @public (undocumented) -export type AuthResult = Authenticated; +export interface AuthNotHandled { + // (undocumented) + type: AuthResultType.notHandled; +} + +// @public (undocumented) +export interface AuthRedirected extends AuthRedirectedParams { + // (undocumented) + type: AuthResultType.redirected; +} + +// @public +export interface AuthRedirectedParams { + headers: { + location: string; + } & ResponseHeaders; +} + +// @public (undocumented) +export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; // @public export interface AuthResultParams { @@ -431,7 +450,11 @@ export interface AuthResultParams { // @public (undocumented) export enum AuthResultType { // (undocumented) - authenticated = "authenticated" + authenticated = "authenticated", + // (undocumented) + notHandled = "notHandled", + // (undocumented) + redirected = "redirected" } // @public @@ -444,6 +467,10 @@ export enum AuthStatus { // @public export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; + notHandled: () => AuthResult; + redirected: (headers: { + location: string; + } & ResponseHeaders) => AuthResult; } // @public @@ -970,6 +997,10 @@ export class KibanaRequest { // @public export interface RouteConfigOptions { - authRequired?: boolean; + authRequired?: boolean | 'optional'; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; xsrfRequired?: Method extends 'get' ? never : boolean; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 35ac4e27f9c8b..8ed64f004c9be 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -25,4 +25,5 @@ export const storybookAliases = { embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js', + ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', }; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.test.tsx index e09f26311e4e3..2278b243ecc14 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/doc.test.tsx @@ -45,7 +45,10 @@ beforeEach(() => { jest.clearAllMocks(); }); -export const waitForPromises = () => new Promise(resolve => setTimeout(resolve, 0)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); /** * this works but logs ugly error messages until we're using React 16.9 diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts index 42d4e04184a25..9e2bfd4023bd9 100644 --- a/src/legacy/core_plugins/timelion/index.ts +++ b/src/legacy/core_plugins/timelion/index.ts @@ -62,14 +62,6 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars(server) { - const config = server.config(); - - return { - timelionUiEnabled: config.get('timelion.ui.enabled'), - kbnIndex: config.get('kibana.index'), - }; - }, mappings: require('./mappings.json'), uiSettingDefaults: { 'timelion:showTutorial': { diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts index 63030fcbce387..acb95e80fe18c 100644 --- a/src/legacy/core_plugins/timelion/public/legacy.ts +++ b/src/legacy/core_plugins/timelion/public/legacy.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; +import { npSetup } from 'ui/new_platform'; import { plugin } from '.'; import { TimelionPluginSetupDependencies } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; @@ -32,4 +32,4 @@ const setupPlugins: Readonly = { const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core); +export const start = pluginInstance.start(); diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts index 636b8bf8e128a..8b021cda4bfb0 100644 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ b/src/legacy/core_plugins/timelion/public/plugin.ts @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - IUiSettingsClient, -} from 'kibana/public'; +import { CoreSetup, Plugin, PluginInitializerContext, IUiSettingsClient } from 'kibana/public'; import { getTimeChart } from './panels/timechart/timechart'; import { Panel } from './panels/panel'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; @@ -65,13 +59,7 @@ export class TimelionPlugin implements Plugin, void> { dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel); } - public start(core: CoreStart) { - const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled'); - - if (timelionUiEnabled === false) { - core.chrome.navLinks.update('timelion', { hidden: true }); - } - } + public start() {} public stop(): void {} } diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 4bf7e1c9c6ba7..03167f3080419 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -18,6 +18,7 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { IIndexPattern, IFieldType } from '../..'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; import { Filter } from '../filters'; @@ -27,7 +28,16 @@ function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { let format = get(indexPattern, ['fields', 'byName', key, 'format']); if (!format && (indexPattern.fields as any).getByName) { // TODO: Why is indexPatterns sometimes a map and sometimes an array? - format = ((indexPattern.fields as any).getByName(key) as IFieldType).format; + const field: IFieldType = (indexPattern.fields as any).getByName(key); + if (!field) { + throw new Error( + i18n.translate('data.filter.filterBar.fieldNotFound', { + defaultMessage: 'Field {key} not found in index pattern {indexPattern}', + values: { key, indexPattern: indexPattern.title }, + }) + ); + } + format = field.format; } return format; } diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 51204e2a61168..24adf0093af95 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -32,6 +32,15 @@ font-style: italic; } +.globalFilterItem-isInvalid { + text-decoration: none; + + .globalFilterLabel__value { + color: $euiColorDanger; + font-weight: $euiFontWeightBold; + } +} + .globalFilterItem-isPinned { position: relative; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index ee6d178b25c22..070631354d8b8 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -41,6 +41,10 @@ export function FilterLabel({ filter, valueLabel }: Props) { prefixText ); + const getValue = (text?: string) => { + return {text}; + }; + if (filter.meta.alias !== null) { return ( @@ -55,35 +59,35 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key} {existsOperator.message} + {filter.meta.key}: {getValue(`${existsOperator.message}`)} ); case FILTERS.GEO_BOUNDING_BOX: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.GEO_POLYGON: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.PHRASES: return ( {prefix} - {filter.meta.key} {isOneOfOperator.message} {valueLabel} + {filter.meta.key}: {getValue(`${isOneOfOperator.message} ${valueLabel}`)} ); case FILTERS.QUERY_STRING: return ( {prefix} - {valueLabel} + {getValue(`${valueLabel}`)} ); case FILTERS.PHRASE: @@ -91,14 +95,14 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); default: return ( {prefix} - {JSON.stringify(filter.query) || filter.meta.value} + {getValue(`${JSON.stringify(filter.query) || filter.meta.value}`)} ); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 0febfe807a946..6b5fd41dc06ea 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -33,6 +33,7 @@ import { toggleFilterPinned, toggleFilterDisabled, } from '../../../common'; +import { getNotifications } from '../../services'; interface Props { id: string; @@ -64,24 +65,41 @@ class FilterItemUI extends Component { public render() { const { filter, id } = this.props; const { negate, disabled } = filter.meta; + let hasError: boolean = false; + + let valueLabel; + try { + valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); + } catch (e) { + getNotifications().toasts.addError(e, { + title: this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorMessage', + defaultMessage: 'Failed to display filter', + }), + }); + valueLabel = this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorText', + defaultMessage: 'Error', + }); + hasError = true; + } + const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; + const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; + const dataTestSubjDisabled = `filter-${ + this.props.filter.meta.disabled ? 'disabled' : 'enabled' + }`; const classes = classNames( 'globalFilterItem', { - 'globalFilterItem-isDisabled': disabled, + 'globalFilterItem-isDisabled': disabled || hasError, + 'globalFilterItem-isInvalid': hasError, 'globalFilterItem-isPinned': isFilterPinned(filter), 'globalFilterItem-isExcluded': negate, }, this.props.className ); - const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); - const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; - const dataTestSubjDisabled = `filter-${ - this.props.filter.meta.disabled ? 'disabled' : 'enabled' - }`; - const badge = ( ; diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index fe6e425f76c05..dddfd6c67e655 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -4,5 +4,5 @@ "kibanaVersion": "kibana", "configPath": ["timelion"], "server": true, - "ui": false + "ui": true } diff --git a/src/plugins/timelion/public/index.ts b/src/plugins/timelion/public/index.ts new file mode 100644 index 0000000000000..b05c4f8a30b22 --- /dev/null +++ b/src/plugins/timelion/public/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { CoreStart, PluginInitializerContext } from 'kibana/public'; +import { ConfigSchema } from '../config'; + +export const plugin = (initializerContext: PluginInitializerContext) => ({ + setup() {}, + start(core: CoreStart) { + if (initializerContext.config.get().ui.enabled === false) { + core.chrome.navLinks.update('timelion', { hidden: true }); + } + }, +}); diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/timelion/server/index.ts index 690544f0b9f5c..5d420327f961e 100644 --- a/src/plugins/timelion/server/index.ts +++ b/src/plugins/timelion/server/index.ts @@ -18,11 +18,18 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { ConfigSchema } from './config'; +import { configSchema } from '../config'; import { Plugin } from './plugin'; export { PluginSetupContract } from './plugin'; -export const config = { schema: ConfigSchema }; +export const config = { + schema: configSchema, + exposeToBrowser: { + ui: { + enabled: true, + }, + }, +}; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/src/plugins/timelion/server/lib/config_manager.ts b/src/plugins/timelion/server/lib/config_manager.ts index 60d89f34a4c08..17471ca34f5ba 100644 --- a/src/plugins/timelion/server/lib/config_manager.ts +++ b/src/plugins/timelion/server/lib/config_manager.ts @@ -19,14 +19,14 @@ import { PluginInitializerContext } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { ConfigSchema } from '../config'; +import { configSchema } from '../../config'; export class ConfigManager { private esShardTimeout: number = 0; private graphiteUrls: string[] = []; constructor(config: PluginInitializerContext['config']) { - config.create>().subscribe(configUpdate => { + config.create>().subscribe(configUpdate => { this.graphiteUrls = configUpdate.graphiteUrls || []; }); diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 4330bc0ffb357..40e89008e7562 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -26,7 +26,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/utils'; -import { ConfigSchema } from './config'; +import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; import { validateEsRoute } from './routes/validate_es'; @@ -48,7 +48,7 @@ export class Plugin { public async setup(core: CoreSetup): Promise> { const config = await this.initializerContext.config - .create>() + .create>() .pipe(first()) .toPromise(); diff --git a/test/api_integration/apis/index_patterns/fields_for_time_pattern_route/query_params.js b/test/api_integration/apis/index_patterns/fields_for_time_pattern_route/query_params.js index 10a5de2c2aa77..2b687a70a6461 100644 --- a/test/api_integration/apis/index_patterns/fields_for_time_pattern_route/query_params.js +++ b/test/api_integration/apis/index_patterns/fields_for_time_pattern_route/query_params.js @@ -93,7 +93,7 @@ export default function({ getService }) { .expect(400) .then(resp => { expect(resp.body.message).to.contain( - '[request query.look_back]: Value is [0] but it must be equal to or greater than [1].' + '[request query.look_back]: Value must be equal to or greater than [1].' ); })); }); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 432e83891aa92..2011b9bc274f5 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -23,7 +23,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); - const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); @@ -188,7 +187,7 @@ export default function({ getService, getPageObjects }) { describe('time zone switch', () => { it('should show bars in the correct time zone after switching', async function() { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); - await browser.refresh(); + await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f2af61df73d20..53628ea970fb6 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -39,7 +39,7 @@ "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", - "xpack.transform": ["legacy/plugins/transform", "plugins/transform"], + "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": "legacy/plugins/uptime", diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 7809734dbf2ad..d5764001a7f18 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -22,6 +22,7 @@ exports[`Home component should render services 1`] = ` }, "notifications": Object { "toasts": Object { + "addDanger": [Function], "addWarning": [Function], }, }, @@ -61,6 +62,7 @@ exports[`Home component should render traces 1`] = ` }, "notifications": Object { "toasts": Object { + "addDanger": [Function], "addWarning": [Function], }, }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx new file mode 100644 index 0000000000000..418430e37b21e --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut } from '@elastic/eui'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; + +const EmptyBannerCallOut = styled(EuiCallOut)` + margin: ${lightTheme.gutterTypes.gutterSmall}; + /* Add some extra margin so it displays to the right of the controls. */ + margin-left: calc( + ${lightTheme.gutterTypes.gutterLarge} + + ${lightTheme.gutterTypes.gutterExtraLarge} + ); + position: absolute; + z-index: 1; +`; + +export function EmptyBanner() { + return ( + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { + defaultMessage: + "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." + })}{' '} + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { + defaultMessage: 'Learn more in the docs' + })} + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx new file mode 100644 index 0000000000000..926f53954e7c6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { render } from '@testing-library/react'; +import React, { FunctionComponent } from 'react'; +import { License } from '../../../../../../../plugins/licensing/common/license'; +import { LicenseContext } from '../../../context/LicenseContext'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; +import { ServiceMap } from './'; + +const expiredLicense = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'expired', + type: 'platinum', + uid: '1' + } +}); + +const Wrapper: FunctionComponent = ({ children }) => { + return ( + + {children} + + ); +}; + +describe('ServiceMap', () => { + describe('with an inactive license', () => { + it('renders the license banner', async () => { + expect( + ( + await render(, { + wrapper: Wrapper + }).findAllByText(/Platinum/) + ).length + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index d5f0728a7ff12..2942ce64729e7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -21,14 +21,15 @@ import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useCallApmApi } from '../../../hooks/useCallApmApi'; import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; import { useLicense } from '../../../hooks/useLicense'; import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; import { getCytoscapeElements } from './get_cytoscape_elements'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; import { Popover } from './Popover'; @@ -61,7 +62,6 @@ ${theme.euiColorLightShade}`, const MAX_REQUESTS = 5; export function ServiceMap({ serviceName }: ServiceMapProps) { - const callApmApi = useCallApmApi(); const license = useLicense(); const { search } = useLocation(); const { urlParams, uiFilters } = useUrlParams(); @@ -137,7 +137,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } } }, - [params, setIsLoading, callApmApi, responses.length, notifications.toasts] + [params, setIsLoading, responses.length, notifications.toasts] ); useEffect(() => { @@ -215,6 +215,9 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > + {serviceName && renderedElements.current.length === 1 && ( + + )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx index 1564f1ae746a9..997df371b51ed 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx @@ -8,10 +8,9 @@ import React, { useState } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { useCallApmApi } from '../../../../../hooks/useCallApmApi'; import { Config } from '../index'; import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration_constants'; -import { APMClient } from '../../../../../services/rest/createCallApmApi'; +import { callApmApi } from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; interface Props { @@ -22,7 +21,6 @@ interface Props { export function DeleteButton({ onDeleted, selectedConfig }: Props) { const [isDeleting, setIsDeleting] = useState(false); const { toasts } = useApmPluginContext().core.notifications; - const callApmApi = useCallApmApi(); return ( { setIsDeleting(true); - await deleteConfig(callApmApi, selectedConfig, toasts); + await deleteConfig(selectedConfig, toasts); setIsDeleting(false); onDeleted(); }} @@ -45,7 +43,6 @@ export function DeleteButton({ onDeleted, selectedConfig }: Props) { } async function deleteConfig( - callApmApi: APMClient, selectedConfig: Config, toasts: NotificationsStart['toasts'] ) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx similarity index 82% rename from x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx index ab3accec90d1d..537bdace50e24 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx @@ -10,12 +10,12 @@ import { i18n } from '@kbn/i18n'; import { omitAllOption, getOptionLabel -} from '../../../../../../../plugins/apm/common/agent_configuration_constants'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { SelectWithPlaceholder } from '../SelectWithPlaceholder'; +} from '../../../../../../../../../plugins/apm/common/agent_configuration_constants'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceForm.selectPlaceholder', + 'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder', { defaultMessage: 'Select' } )} -`; @@ -27,7 +27,7 @@ interface Props { onEnvironmentChange: (env: string) => void; } -export function ServiceForm({ +export function ServiceSection({ isReadOnly, serviceName, onServiceNameChange, @@ -60,7 +60,7 @@ export function ServiceForm({ ); const ALREADY_CONFIGURED_TRANSLATED = i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceForm.alreadyConfiguredOption', + 'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption', { defaultMessage: 'already configured' } ); @@ -83,7 +83,7 @@ export function ServiceForm({

{i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceForm.title', + 'xpack.apm.settings.agentConf.flyOut.serviceSection.title', { defaultMessage: 'Service' } )}

@@ -93,13 +93,13 @@ export function ServiceForm({ - ; }) { await callApmApi({ @@ -94,11 +91,11 @@ export function ApmIndices() { const [apmIndices, setApmIndices] = useState>({}); const [isSaving, setIsSaving] = useState(false); - const callApmApiFromHook = useCallApmApi(); - const { data = INITIAL_STATE, status, refetch } = useFetcher( - callApmApi => - callApmApi({ pathname: `/api/apm/settings/apm-index-settings` }), + _callApmApi => + _callApmApi({ + pathname: `/api/apm/settings/apm-index-settings` + }), [] ); @@ -122,10 +119,7 @@ export function ApmIndices() { event.preventDefault(); setIsSaving(true); try { - await saveApmIndices({ - callApmApi: callApmApiFromHook, - apmIndices - }); + await saveApmIndices({ apmIndices }); toasts.addSuccess({ title: i18n.translate( 'xpack.apm.settings.apmIndices.applyChanges.succeeded.title', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx deleted file mode 100644 index 8cb604d367549..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx +++ /dev/null @@ -1,81 +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 { EuiFieldText, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface Props { - label: string; - onLabelChange: (label: string) => void; - url: string; - onURLChange: (url: string) => void; -} - -export const SettingsSection = ({ - label, - onLabelChange, - url, - onURLChange -}: Props) => { - return ( - <> - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.flyout.settingsSection.title', - { defaultMessage: 'Action' } - )} -

-
- - - { - onLabelChange(e.target.value); - }} - /> - - - { - onURLChange(e.target.value); - }} - /> - - - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx deleted file mode 100644 index d04cdd62c303b..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx +++ /dev/null @@ -1,109 +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 { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiText, - EuiTitle -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { SettingsSection } from './SettingsSection'; -import { ServiceForm } from '../../../../../shared/ServiceForm'; - -interface Props { - onClose: () => void; -} - -export const CustomActionsFlyout = ({ onClose }: Props) => { - const [serviceName, setServiceName] = useState(''); - const [environment, setEnvironment] = useState(''); - const [label, setLabel] = useState(''); - const [url, setURL] = useState(''); - return ( - - - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.flyout.title', - { - defaultMessage: 'Create custom action' - } - )} -

-
-
- - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.flyout.label', - { - defaultMessage: - "This action will be shown in the 'Actions' context menu for the trace and error detail components. You can specify any number of links, but only the first three will be shown, in alphabetical order." - } - )} -

-
- - - - - - -
- - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.flyout.close', - { - defaultMessage: 'Close' - } - )} - - - - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.flyout.save', - { - defaultMessage: 'Save' - } - )} - - - - -
-
- ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx deleted file mode 100644 index f39e4b307b24c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx +++ /dev/null @@ -1,52 +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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -export const EmptyPrompt = ({ - onCreateCustomActionClick -}: { - onCreateCustomActionClick: () => void; -}) => { - return ( - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.emptyPromptTitle', - { - defaultMessage: 'No actions found.' - } - )} - - } - body={ - <> -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.emptyPromptText', - { - defaultMessage: - "Let's change that! You can add custom actions to the Actions context menu by the trace and error details for each service. This could be linking to a Kibana dashboard or going to your organization's support portal" - } - )} -

- - } - actions={ - - {i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.createCustomAction', - { defaultMessage: 'Create custom action' } - )} - - } - /> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx deleted file mode 100644 index 970de66c64a9a..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; -import { CustomActionsOverview } from '../'; -import { expectTextsInDocument } from '../../../../../../utils/testHelpers'; -import * as hooks from '../../../../../../hooks/useFetcher'; - -describe('CustomActions', () => { - afterEach(() => jest.restoreAllMocks()); - - describe('empty prompt', () => { - it('shows when any actions are available', () => { - // TODO: mock return items - const component = render(); - expectTextsInDocument(component, ['No actions found.']); - }); - it('opens flyout when click to create new action', () => { - spyOn(hooks, 'useFetcher').and.returnValue({ - data: [], - status: 'success' - }); - const { queryByText, getByText } = render(); - expect(queryByText('Service')).not.toBeInTheDocument(); - fireEvent.click(getByText('Create custom action')); - expect(queryByText('Service')).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx deleted file mode 100644 index ae2972f251fc2..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx +++ /dev/null @@ -1,75 +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 { EuiPanel, EuiSpacer } from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import React, { useState } from 'react'; -import { ManagedTable } from '../../../../shared/ManagedTable'; -import { Title } from './Title'; -import { EmptyPrompt } from './EmptyPrompt'; -import { CustomActionsFlyout } from './CustomActionsFlyout'; - -export const CustomActionsOverview = () => { - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - - // TODO: change it to correct fields fetched from ES - const columns = [ - { - field: 'actionName', - name: 'Action Name', - truncateText: true - }, - { - field: 'serviceName', - name: 'Service Name' - }, - { - field: 'environment', - name: 'Environment' - }, - { - field: 'lastUpdate', - name: 'Last update' - }, - { - field: 'actions', - name: 'Actions' - } - ]; - - // TODO: change to items fetched from ES. - const items: object[] = []; - - const onCloseFlyout = () => { - setIsFlyoutOpen(false); - }; - - const onCreateCustomActionClick = () => { - setIsFlyoutOpen(true); - }; - - return ( - <> - - - <EuiSpacer size="m" /> - {isFlyoutOpen && <CustomActionsFlyout onClose={onCloseFlyout} />} - {isEmpty(items) ? ( - <EmptyPrompt onCreateCustomActionClick={onCreateCustomActionClick} /> - ) : ( - <ManagedTable - items={items} - columns={columns} - initialPageSize={25} - initialSortField="occurrenceCount" - initialSortDirection="desc" - sortItems={false} - /> - )} - </EuiPanel> - </> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx new file mode 100644 index 0000000000000..415d2557c23c3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -0,0 +1,21 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const CreateCustomLinkButton = ({ + onClick +}: { + onClick: () => void; +}) => ( + <EuiButton color="primary" fill onClick={onClick}> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.createCustomLink', + { defaultMessage: 'Create custom link' } + )} + </EuiButton> +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx new file mode 100644 index 0000000000000..2b3a5cbe87992 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; +import React, { useState } from 'react'; +import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; + +interface Props { + onDelete: () => void; + customLinkId: string; +} + +export function DeleteButton({ onDelete, customLinkId }: Props) { + const [isDeleting, setIsDeleting] = useState(false); + const { toasts } = useApmPluginContext().core.notifications; + + return ( + <EuiButtonEmpty + color="danger" + isLoading={isDeleting} + iconSide="right" + onClick={async () => { + setIsDeleting(true); + await deleteConfig(customLinkId, toasts); + setIsDeleting(false); + onDelete(); + }} + > + {i18n.translate('xpack.apm.settings.customizeUI.customLink.delete', { + defaultMessage: 'Delete' + })} + </EuiButtonEmpty> + ); +} + +async function deleteConfig( + customLinkId: string, + toasts: NotificationsStart['toasts'] +) { + try { + await callApmApi({ + pathname: '/api/apm/settings/custom_links/{id}', + method: 'DELETE', + params: { + path: { id: customLinkId } + } + }); + toasts.addSuccess({ + iconType: 'trash', + title: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.delete.successed', + { defaultMessage: 'Deleted custom link.' } + ) + }); + } catch (error) { + toasts.addDanger({ + iconType: 'cross', + title: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.delete.failed', + { defaultMessage: 'Custom link could not be deleted' } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx new file mode 100644 index 0000000000000..69fecf25f5143 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx @@ -0,0 +1,167 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import React from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FilterOptions } from '../../../../../../../../../../plugins/apm/server/routes/settings/custom_link'; +import { + DEFAULT_OPTION, + Filters, + filterSelectOptions, + getSelectOptions +} from './helper'; + +export const FiltersSection = ({ + filters, + onChangeFilters +}: { + filters: Filters; + onChangeFilters: (filters: Filters) => void; +}) => { + const onChangeFilter = (filter: Filters[0], idx: number) => { + const newFilters = [...filters]; + newFilters[idx] = filter; + onChangeFilters(newFilters); + }; + + const onRemoveFilter = (idx: number) => { + // remove without mutating original array + const newFilters = [...filters].splice(idx, 1); + + // if there is only one item left it should not be removed + // but reset to empty + if (isEmpty(newFilters)) { + onChangeFilters([['', '']]); + } else { + onChangeFilters(newFilters); + } + }; + + const handleAddFilter = () => { + onChangeFilters([...filters, ['', '']]); + }; + + return ( + <> + <EuiTitle size="xs"> + <h3> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.filters.title', + { + defaultMessage: 'Filters' + } + )} + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText size="xs"> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.filters.subtitle', + { + defaultMessage: + 'Add additional values within the same field by comma separating values.' + } + )} + </EuiText> + + <EuiSpacer size="s" /> + + {filters.map((filter, idx) => { + const [key, value] = filter; + const filterId = `filter-${idx}`; + const selectOptions = getSelectOptions(filters, idx); + return ( + <EuiFlexGroup key={filterId} gutterSize="s" alignItems="center"> + <EuiFlexItem> + <EuiSelect + aria-label={filterId} + id={filterId} + fullWidth + options={selectOptions} + value={key} + prepend={i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.filters.prepend', + { + defaultMessage: 'Field' + } + )} + onChange={e => + onChangeFilter( + [e.target.value as keyof FilterOptions, value], + idx + ) + } + isInvalid={ + !isEmpty(value) && + (isEmpty(key) || key === DEFAULT_OPTION.value) + } + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiFieldText + fullWidth + placeholder={i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyOut.filters.defaultOption.value', + { defaultMessage: 'Value' } + )} + onChange={e => onChangeFilter([key, e.target.value], idx)} + value={value} + isInvalid={!isEmpty(key) && isEmpty(value)} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="trash" + onClick={() => onRemoveFilter(idx)} + disabled={!key && filters.length === 1} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); + })} + + <EuiSpacer size="xs" /> + + <AddFilterButton + onClick={handleAddFilter} + // Disable button when user has already added all items available + isDisabled={filters.length === filterSelectOptions.length - 1} + /> + </> + ); +}; + +const AddFilterButton = ({ + onClick, + isDisabled +}: { + onClick: () => void; + isDisabled: boolean; +}) => ( + <EuiButtonEmpty + iconType="plusInCircle" + onClick={onClick} + disabled={isDisabled} + > + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.filters.addAnotherFilter', + { + defaultMessage: 'Add another filter' + } + )} + </EuiButtonEmpty> +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx new file mode 100644 index 0000000000000..cb27221309812 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx @@ -0,0 +1,70 @@ +/* + * 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 { + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DeleteButton } from './DeleteButton'; + +export const FlyoutFooter = ({ + onClose, + isSaving, + onDelete, + customLinkId, + isSaveButtonEnabled +}: { + onClose: () => void; + isSaving: boolean; + onDelete: () => void; + customLinkId?: string; + isSaveButtonEnabled: boolean; +}) => { + return ( + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.close', + { + defaultMessage: 'Close' + } + )} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup> + {customLinkId && ( + <EuiFlexItem> + <DeleteButton customLinkId={customLinkId} onDelete={onDelete} /> + </EuiFlexItem> + )} + <EuiFlexItem> + <EuiButton + fill + type="submit" + isLoading={isSaving} + isDisabled={!isSaveButtonEnabled} + > + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.save', + { + defaultMessage: 'Save' + } + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx new file mode 100644 index 0000000000000..89f55a6c682ca --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx @@ -0,0 +1,135 @@ +/* + * 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 { + EuiFieldText, + EuiFormRow, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CustomLink } from '../../../../../../../../../../plugins/apm/server/lib/settings/custom_link/custom_link_types'; + +interface InputField { + name: keyof CustomLink; + label: string; + helpText: string; + placeholder: string; + onChange: (value: string) => void; + value?: string; +} + +interface Props { + label?: string; + onChangeLabel: (label: string) => void; + url?: string; + onChangeUrl: (url: string) => void; +} + +export const LinkSection = ({ + label, + onChangeLabel, + url, + onChangeUrl +}: Props) => { + const inputFields: InputField[] = [ + { + name: 'label', + label: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.label', + { + defaultMessage: 'Label' + } + ), + helpText: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.label.helpText', + { + defaultMessage: + 'This is the label shown in the actions context menu. Keep it as short as possible.' + } + ), + placeholder: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.label.placeholder', + { + defaultMessage: 'e.g. Support tickets' + } + ), + value: label, + onChange: onChangeLabel + }, + { + name: 'url', + label: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.url', + { + defaultMessage: 'URL' + } + ), + helpText: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.url.helpText', + { + defaultMessage: + 'Add fieldname variables to your URL to apply values e.g. {sample}. TODO: Learn more in the docs.', + values: { sample: '{{trace.id}}' } + } + ), + placeholder: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.link.url.placeholder', + { + defaultMessage: 'e.g. https://www.elastic.co/' + } + ), + value: url, + onChange: onChangeUrl + } + ]; + + return ( + <> + <EuiTitle size="xs"> + <h3> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.action.title', + { + defaultMessage: 'Link' + } + )} + </h3> + </EuiTitle> + <EuiSpacer size="l" /> + {inputFields.map(field => { + return ( + <EuiFormRow + fullWidth + key={field.name} + label={field.label} + helpText={field.helpText} + labelAppend={ + <EuiText size="xs"> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.required', + { + defaultMessage: 'Required' + } + )} + </EuiText> + } + > + <EuiFieldText + placeholder={field.placeholder} + name={field.name} + fullWidth + value={field.value} + onChange={e => field.onChange(e.target.value)} + aria-label={field.name} + /> + </EuiFormRow> + ); + })} + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts new file mode 100644 index 0000000000000..bb86a251594ab --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts @@ -0,0 +1,96 @@ +/* + * 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 { isEmpty, pick } from 'lodash'; +import { + FilterOptions, + filterOptions + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../../../../plugins/apm/server/routes/settings/custom_link'; +import { CustomLink } from '../../../../../../../../../../plugins/apm/server/lib/settings/custom_link/custom_link_types'; + +export type Filters = Array<[keyof FilterOptions | '', string]>; + +interface FilterSelectOption { + value: 'DEFAULT' | keyof FilterOptions; + text: string; +} + +/** + * Converts available filters from the Custom Link to Array of filters. + * e.g. + * customLink = { + * id: '1', + * label: 'foo', + * url: 'http://www.elastic.co', + * service.name: 'opbeans-java', + * transaction.type: 'request' + * } + * + * results: [['service.name', 'opbeans-java'],['transaction.type', 'request']] + * @param customLink + */ +export const convertFiltersToArray = (customLink?: CustomLink): Filters => { + if (customLink) { + const filters = Object.entries(pick(customLink, filterOptions)) as Filters; + if (!isEmpty(filters)) { + return filters; + } + } + return [['', '']]; +}; + +/** + * Converts array of filters into object. + * e.g. + * filters: [['service.name', 'opbeans-java'],['transaction.type', 'request']] + * + * results: { + * 'service.name': 'opbeans-java', + * 'transaction.type': 'request' + * } + * @param filters + */ +export const convertFiltersToObject = (filters: Filters) => { + const convertedFilters = Object.fromEntries( + filters.filter(([key, value]) => !isEmpty(key) && !isEmpty(value)) + ); + if (!isEmpty(convertedFilters)) { + return convertedFilters; + } +}; + +export const DEFAULT_OPTION: FilterSelectOption = { + value: 'DEFAULT', + text: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyOut.filters.defaultOption', + { defaultMessage: 'Select field...' } + ) +}; + +export const filterSelectOptions: FilterSelectOption[] = [ + DEFAULT_OPTION, + ...filterOptions.map(filter => ({ + value: filter as keyof FilterOptions, + text: filter + })) +]; + +/** + * Returns the options available, removing filters already added, but keeping the selected filter. + * + * @param filters + * @param idx + */ +export const getSelectOptions = (filters: Filters, idx: number) => { + return filterSelectOptions.filter(option => { + const indexUsedFilter = filters.findIndex( + filter => filter[0] === option.value + ); + // Filter out all items already added, besides the one selected in the current filter. + return indexUsedFilter === -1 || idx === indexUsedFilter; + }); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx new file mode 100644 index 0000000000000..88358c888160b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { CustomLink } from '../../../../../../../../../../plugins/apm/server/lib/settings/custom_link/custom_link_types'; +import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { FiltersSection } from './FiltersSection'; +import { FlyoutFooter } from './FlyoutFooter'; +import { LinkSection } from './LinkSection'; +import { saveCustomLink } from './saveCustomLink'; +import { convertFiltersToArray, convertFiltersToObject } from './helper'; + +interface Props { + onClose: () => void; + customLinkSelected?: CustomLink; + onSave: () => void; + onDelete: () => void; +} + +export const CustomLinkFlyout = ({ + onClose, + customLinkSelected, + onSave, + onDelete +}: Props) => { + const { toasts } = useApmPluginContext().core.notifications; + const [isSaving, setIsSaving] = useState(false); + + const [label, setLabel] = useState(customLinkSelected?.label || ''); + const [url, setUrl] = useState(customLinkSelected?.url || ''); + const [filters, setFilters] = useState( + convertFiltersToArray(customLinkSelected) + ); + + const isFormValid = !!label && !!url; + + const onSubmit = async ( + event: + | React.FormEvent<HTMLFormElement> + | React.MouseEvent<HTMLButtonElement> + ) => { + event.preventDefault(); + setIsSaving(true); + await saveCustomLink({ + id: customLinkSelected?.id, + label, + url, + filters: convertFiltersToObject(filters), + toasts + }); + setIsSaving(false); + onSave(); + }; + + return ( + <EuiPortal> + <form onSubmit={onSubmit}> + <EuiFlyout ownFocus onClose={onClose} size="m"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="s"> + <h2> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.title', + { + defaultMessage: 'Create link' + } + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiText> + <p> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.label', + { + defaultMessage: + 'Links will be available in the context of transaction details throughout the APM app. You can create an unlimited number of links and use the filter options to scope them to only appear for specific services. You can refer to dynamic variables by using any of the transaction metadata to fill in your URLs. TODO: Learn more about it in the docs.' + } + )} + </p> + </EuiText> + + <EuiSpacer size="l" /> + + <LinkSection + label={label} + onChangeLabel={setLabel} + url={url} + onChangeUrl={setUrl} + /> + + <EuiSpacer size="l" /> + + <FiltersSection filters={filters} onChangeFilters={setFilters} /> + </EuiFlyoutBody> + + <FlyoutFooter + isSaveButtonEnabled={isFormValid} + onClose={onClose} + isSaving={isSaving} + onDelete={onDelete} + customLinkId={customLinkSelected?.id} + /> + </EuiFlyout> + </form> + </EuiPortal> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts new file mode 100644 index 0000000000000..f255840e1d734 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts @@ -0,0 +1,73 @@ +/* + * 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 { NotificationsStart } from 'kibana/public'; +import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; + +export async function saveCustomLink({ + id, + label, + url, + filters, + toasts +}: { + id?: string; + label: string; + url: string; + filters?: { [key: string]: string }; + toasts: NotificationsStart['toasts']; +}) { + try { + const customLink = { + label, + url, + ...filters + }; + if (id) { + await callApmApi({ + pathname: '/api/apm/settings/custom_links/{id}', + method: 'PUT', + params: { + path: { id }, + body: customLink + } + }); + } else { + await callApmApi({ + pathname: '/api/apm/settings/custom_links', + method: 'POST', + params: { + body: customLink + } + }); + } + toasts.addSuccess({ + iconType: 'check', + title: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.create.successed', + { defaultMessage: 'Link saved!' } + ) + }); + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.create.failed', + { defaultMessage: 'Link could not be saved!' } + ), + text: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.create.failed.message', + { + defaultMessage: + 'Something went wrong when saving the link. Error: "{errorMessage}"', + values: { + errorMessage: error.message + } + } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx new file mode 100644 index 0000000000000..f7d8c4baa71e9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { units, px } from '../../../../../style/variables'; +import { CustomLink } from '../../../../../../../../../plugins/apm/server/lib/settings/custom_link/custom_link_types'; +import { ManagedTable } from '../../../../shared/ManagedTable'; +import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; +import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; + +interface Props { + items: CustomLink[]; + onCustomLinkSelected: (customLink: CustomLink) => void; +} + +export const CustomLinkTable = ({ + items = [], + onCustomLinkSelected +}: Props) => { + const [searchTerm, setSearchTerm] = useState(''); + + const columns = [ + { + field: 'label', + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.name', + { defaultMessage: 'Name' } + ), + truncateText: true + }, + { + field: 'url', + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.url', + { defaultMessage: 'URL' } + ), + truncateText: true + }, + { + width: px(160), + align: 'right', + field: '@timestamp', + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.lastUpdated', + { defaultMessage: 'Last updated' } + ), + sortable: true, + render: (value: number) => ( + <TimestampTooltip time={value} timeUnit="minutes" /> + ) + }, + { + width: px(units.triple), + name: '', + actions: [ + { + name: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel', + { defaultMessage: 'Edit' } + ), + description: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription', + { defaultMessage: 'Edit this custom link' } + ), + icon: 'pencil', + color: 'primary', + type: 'icon', + onClick: (customLink: CustomLink) => { + onCustomLinkSelected(customLink); + } + } + ] + } + ]; + + const filteredItems = items.filter(({ label, url }) => { + return ( + label.toLowerCase().includes(searchTerm) || + url.toLowerCase().includes(searchTerm) + ); + }); + + return ( + <> + <EuiSpacer size="m" /> + <EuiFieldSearch + fullWidth + onChange={e => setSearchTerm(e.target.value)} + placeholder={i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.searchInput.filter', + { + defaultMessage: 'Filter links by Name and URL...' + } + )} + /> + <EuiSpacer size="s" /> + <ManagedTable + noItemsMessage={ + isEmpty(items) ? ( + <LoadingStatePrompt /> + ) : ( + <NoResultFound value={searchTerm} /> + ) + } + items={filteredItems} + columns={columns} + initialPageSize={10} + initialSortField="@timestamp" + initialSortDirection="desc" + /> + </> + ); +}; + +const NoResultFound = ({ value }: { value: string }) => ( + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiText size="s"> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.table.noResultFound', + { + defaultMessage: `No results for "{value}".`, + values: { value } + } + )} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx new file mode 100644 index 0000000000000..e75004918f430 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; + +export const EmptyPrompt = ({ + onCreateCustomLinkClick +}: { + onCreateCustomLinkClick: () => void; +}) => { + return ( + <EuiEmptyPrompt + iconType="link" + iconColor="" + title={ + <h2> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.emptyPromptTitle', + { + defaultMessage: 'No links found.' + } + )} + </h2> + } + body={ + <> + <p> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.emptyPromptText', + { + defaultMessage: + "Let's change that! You can add custom links to the Actions context menu by the transaction details for each service. Create a helpful link to your company's support portal or open a new bug report. Learn more about it in our docs." + } + )} + </p> + </> + } + actions={<CreateCustomLinkButton onClick={onCreateCustomLinkClick} />} + /> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx similarity index 81% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx rename to x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx index d7f90e0919733..17ec42b3e2016 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx @@ -14,8 +14,8 @@ export const Title = () => ( <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> <EuiFlexItem grow={false}> <h1> - {i18n.translate('xpack.apm.settings.customizeUI.customActions', { - defaultMessage: 'Custom actions' + {i18n.translate('xpack.apm.settings.customizeUI.customLink', { + defaultMessage: 'Custom Links' })} </h1> </EuiFlexItem> @@ -25,10 +25,10 @@ export const Title = () => ( type="iInCircle" position="top" content={i18n.translate( - 'xpack.apm.settings.customizeUI.customActions.info', + 'xpack.apm.settings.customizeUI.customLink.info', { defaultMessage: - "These actions will be shown in the 'Actions' context menu for the trace and error detail components." + "These links will be shown in the 'Actions' context menu for the transaction detail." } )} /> diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/__test__/CustomLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/__test__/CustomLink.test.tsx new file mode 100644 index 0000000000000..f02cc2be8268d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/__test__/CustomLink.test.tsx @@ -0,0 +1,251 @@ +/* + * 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 { fireEvent, render, wait } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { CustomLinkOverview } from '../'; +import * as hooks from '../../../../../../hooks/useFetcher'; +import { + expectTextsInDocument, + MockApmPluginContextWrapper +} from '../../../../../../utils/testHelpers'; +import * as saveCustomLink from '../CustomLinkFlyout/saveCustomLink'; +import * as apmApi from '../../../../../../services/rest/createCallApmApi'; + +const data = [ + { + id: '1', + label: 'label 1', + url: 'url 1', + 'service.name': 'opbeans-java' + }, + { + id: '2', + label: 'label 2', + url: 'url 2', + 'transaction.type': 'request' + } +]; + +describe('CustomLink', () => { + describe('empty prompt', () => { + beforeAll(() => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data: [], + status: 'success' + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + it('shows when no link is available', () => { + const component = render(<CustomLinkOverview />); + expectTextsInDocument(component, ['No links found.']); + }); + it('opens flyout when click to create new link', () => { + const { queryByText, getByText } = render( + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + ); + expect(queryByText('Create link')).not.toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('Create custom link')); + }); + expect(queryByText('Create link')).toBeInTheDocument(); + }); + }); + + describe('overview', () => { + beforeAll(() => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data, + status: 'success' + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('shows a table with all custom link', () => { + const component = render( + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + ); + expectTextsInDocument(component, [ + 'label 1', + 'url 1', + 'label 2', + 'url 2' + ]); + }); + + it('checks if create custom link button is available and working', () => { + const { queryByText, getByText } = render( + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + ); + expect(queryByText('Create link')).not.toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('Create custom link')); + }); + expect(queryByText('Create link')).toBeInTheDocument(); + }); + }); + + describe('Flyout', () => { + const refetch = jest.fn(); + let callApmApiSpy: Function; + let saveCustomLinkSpy: Function; + beforeAll(() => { + callApmApiSpy = spyOn(apmApi, 'callApmApi'); + saveCustomLinkSpy = spyOn(saveCustomLink, 'saveCustomLink'); + spyOn(hooks, 'useFetcher').and.returnValue({ + data, + status: 'success', + refetch + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + + const openFlyout = () => { + const component = render( + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + ); + expect(component.queryByText('Create link')).not.toBeInTheDocument(); + act(() => { + fireEvent.click(component.getByText('Create custom link')); + }); + expect(component.queryByText('Create link')).toBeInTheDocument(); + return component; + }; + + it('creates a custom link', async () => { + const component = openFlyout(); + const labelInput = component.getByLabelText('label'); + act(() => { + fireEvent.change(labelInput, { + target: { value: 'foo' } + }); + }); + const urlInput = component.getByLabelText('url'); + act(() => { + fireEvent.change(urlInput, { + target: { value: 'bar' } + }); + }); + await act(async () => { + await wait(() => fireEvent.submit(component.getByText('Save'))); + }); + expect(saveCustomLinkSpy).toHaveBeenCalledTimes(1); + }); + + it('deletes a custom link', async () => { + const component = render( + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + ); + expect(component.queryByText('Create link')).not.toBeInTheDocument(); + const editButtons = component.getAllByLabelText('Edit'); + expect(editButtons.length).toEqual(2); + act(() => { + fireEvent.click(editButtons[0]); + }); + expect(component.queryByText('Create link')).toBeInTheDocument(); + await act(async () => { + await wait(() => fireEvent.click(component.getByText('Delete'))); + }); + expect(callApmApiSpy).toHaveBeenCalled(); + expect(refetch).toHaveBeenCalled(); + }); + + describe('Filters', () => { + const addFilterField = ( + component: ReturnType<typeof openFlyout>, + amount: number + ) => { + for (let i = 1; i <= amount; i++) { + fireEvent.click(component.getByText('Add another filter')); + } + }; + it('checks if add filter button is disabled after all elements have been added', () => { + const component = openFlyout(); + expect(component.getAllByText('service.name').length).toEqual(1); + addFilterField(component, 1); + expect(component.getAllByText('service.name').length).toEqual(2); + addFilterField(component, 2); + expect(component.getAllByText('service.name').length).toEqual(4); + // After 4 items, the button is disabled + addFilterField(component, 2); + expect(component.getAllByText('service.name').length).toEqual(4); + }); + it('removes items already selected', () => { + const component = openFlyout(); + + const addFieldAndCheck = ( + fieldName: string, + selectValue: string, + addNewFilter: boolean, + optionsExpected: string[] + ) => { + if (addNewFilter) { + addFilterField(component, 1); + } + const field = component.getByLabelText( + fieldName + ) as HTMLSelectElement; + const optionsAvailable = Object.values(field) + .map(option => (option as HTMLOptionElement).text) + .filter(option => option); + + act(() => { + fireEvent.change(field, { + target: { value: selectValue } + }); + }); + expect(field.value).toEqual(selectValue); + expect(optionsAvailable).toEqual(optionsExpected); + }; + + addFieldAndCheck('filter-0', 'transaction.name', false, [ + 'Select field...', + 'service.name', + 'service.environment', + 'transaction.type', + 'transaction.name' + ]); + + addFieldAndCheck('filter-1', 'service.name', true, [ + 'Select field...', + 'service.name', + 'service.environment', + 'transaction.type' + ]); + + addFieldAndCheck('filter-2', 'transaction.type', true, [ + 'Select field...', + 'service.environment', + 'transaction.type' + ]); + + addFieldAndCheck('filter-3', 'service.environment', true, [ + 'Select field...', + 'service.environment' + ]); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx new file mode 100644 index 0000000000000..bc1882c8c2785 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { CustomLink } from '../../../../../../../../../plugins/apm/server/lib/settings/custom_link/custom_link_types'; +import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { CustomLinkFlyout } from './CustomLinkFlyout'; +import { CustomLinkTable } from './CustomLinkTable'; +import { EmptyPrompt } from './EmptyPrompt'; +import { Title } from './Title'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; + +export const CustomLinkOverview = () => { + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const [customLinkSelected, setCustomLinkSelected] = useState< + CustomLink | undefined + >(); + + const { data: customLinks, status, refetch } = useFetcher( + callApmApi => callApmApi({ pathname: '/api/apm/settings/custom_links' }), + [] + ); + + useEffect(() => { + if (customLinkSelected) { + setIsFlyoutOpen(true); + } + }, [customLinkSelected]); + + const onCloseFlyout = () => { + setCustomLinkSelected(undefined); + setIsFlyoutOpen(false); + }; + + const onCreateCustomLinkClick = () => { + setIsFlyoutOpen(true); + }; + + const showEmptyPrompt = + status === FETCH_STATUS.SUCCESS && isEmpty(customLinks); + + return ( + <> + {isFlyoutOpen && ( + <CustomLinkFlyout + onClose={onCloseFlyout} + customLinkSelected={customLinkSelected} + onSave={() => { + onCloseFlyout(); + refetch(); + }} + onDelete={() => { + onCloseFlyout(); + refetch(); + }} + /> + )} + <EuiPanel> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <Title /> + </EuiFlexItem> + {!showEmptyPrompt && ( + <EuiFlexItem> + <EuiFlexGroup alignItems="center" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <CreateCustomLinkButton onClick={onCreateCustomLinkClick} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + )} + </EuiFlexGroup> + + <EuiSpacer size="m" /> + + {showEmptyPrompt ? ( + <EmptyPrompt onCreateCustomLinkClick={onCreateCustomLinkClick} /> + ) : ( + <CustomLinkTable + items={customLinks} + onCustomLinkSelected={setCustomLinkSelected} + /> + )} + </EuiPanel> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx index 17a4b2f847679..1cd1298fdd549 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CustomActionsOverview } from './CustomActionsOverview'; +import { CustomLinkOverview } from './CustomLink'; export const CustomizeUI = () => { return ( @@ -20,7 +20,7 @@ export const CustomizeUI = () => { </h1> </EuiTitle> <EuiSpacer size="l" /> - <CustomActionsOverview /> + <CustomLinkOverview /> </> ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 7645162ab2655..0e0c318ad3299 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -23,7 +23,7 @@ export function ElasticDocsLink({ section, path, children, ...rest }: Props) { children(href) ) : ( <EuiLink href={href} {...rest}> - children + {children} </EuiLink> ); } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts b/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts deleted file mode 100644 index b28b295d8189e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts +++ /dev/null @@ -1,17 +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 { useMemo } from 'react'; -import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { useApmPluginContext } from './useApmPluginContext'; - -export function useCallApmApi() { - const { http } = useApmPluginContext().core; - - return useMemo(() => { - return createCallApmApi(http); - }, [http]); -} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index d2202fff996b1..c2530d6982c3b 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; -import { APMClient } from '../services/rest/createCallApmApi'; -import { useCallApmApi } from './useCallApmApi'; +import { APMClient, callApmApi } from '../services/rest/createCallApmApi'; import { useApmPluginContext } from './useApmPluginContext'; import { useLoadingIndicator } from './useLoadingIndicator'; @@ -46,8 +45,6 @@ export function useFetcher<TReturn>( const { preservePreviousData = true } = options; const { setIsLoading } = useLoadingIndicator(); - const callApmApi = useCallApmApi(); - const { dispatchStatus } = useContext(LoadingIndicatorContext); const [result, setResult] = useState<Result<InferResponseType<TReturn>>>({ data: undefined, 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 0054f963ba8f2..0103dd72a3fea 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -39,6 +39,7 @@ import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { setReadonlyBadge } from './updateBadge'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; +import { createCallApmApi } from '../services/rest/createCallApmApi'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -104,6 +105,7 @@ export class ApmPlugin public start(core: CoreStart) { const i18nCore = core.i18n; const plugins = this.setupPlugins; + createCallApmApi(core.http); // Once we're actually an NP plugin we'll get the config from the // initializerContext like: @@ -157,7 +159,7 @@ export class ApmPlugin ); // create static index pattern and store as saved object. Not needed by APM UI but for legacy reasons in Discover, Dashboard etc. - createStaticIndexPattern(core.http).catch(e => { + createStaticIndexPattern().catch(e => { // eslint-disable-next-line no-console console.log('Error fetching static index pattern', e); }); diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts index 9cca9469bba0e..2d4fd83003179 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts @@ -5,7 +5,7 @@ */ import * as callApiExports from '../rest/callApi'; -import { createCallApmApi, APMClient } from '../rest/createCallApmApi'; +import { createCallApmApi, callApmApi } from '../rest/createCallApmApi'; import { HttpSetup } from 'kibana/public'; const callApi = jest @@ -13,9 +13,8 @@ const callApi = jest .mockImplementation(() => Promise.resolve(null)); describe('callApmApi', () => { - let callApmApi: APMClient; beforeEach(() => { - callApmApi = createCallApmApi({} as HttpSetup); + createCallApmApi({} as HttpSetup); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts index 220320216788a..2fffb40d353fc 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts @@ -19,8 +19,14 @@ export type APMClientOptions = Omit<FetchOptions, 'query' | 'body'> & { }; }; -export const createCallApmApi = (http: HttpSetup) => - ((options: APMClientOptions) => { +export let callApmApi: APMClient = () => { + throw new Error( + 'callApmApi has to be initialized before used. Call createCallApmApi first.' + ); +}; + +export function createCallApmApi(http: HttpSetup) { + callApmApi = ((options: APMClientOptions) => { const { pathname, params = {}, ...opts } = options; const path = (params.path || {}) as Record<string, any>; @@ -36,3 +42,4 @@ export const createCallApmApi = (http: HttpSetup) => query: params.query }); }) as APMClient; +} diff --git a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts index 8e1234dd55e69..1efcc98bbbd66 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; -import { createCallApmApi } from './createCallApmApi'; +import { callApmApi } from './createCallApmApi'; -export const createStaticIndexPattern = async (http: HttpSetup) => { - const callApmApi = createCallApmApi(http); +export const createStaticIndexPattern = async () => { return await callApmApi({ method: 'POST', pathname: '/api/apm/index_pattern/static' diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts index 5e64d7e1ce716..1c618098b36e3 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts @@ -16,7 +16,7 @@ import { } from '../../../../../../plugins/apm/common/ml_job_constants'; import { callApi } from './callApi'; import { ESFilter } from '../../../../../../plugins/apm/typings/elasticsearch'; -import { createCallApmApi, APMClient } from './createCallApmApi'; +import { callApmApi } from './createCallApmApi'; interface MlResponseItem { id: string; @@ -36,7 +36,6 @@ interface StartedMLJobApiResponse { } async function getTransactionIndices(http: HttpSetup) { - const callApmApi: APMClient = createCallApmApi(http); const indices = await callApmApi({ method: 'GET', pathname: `/api/apm/settings/apm-indices` diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index dec2257746e50..4ee45f7b3330b 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -29,6 +29,7 @@ import { ApmPluginContextValue } from '../context/ApmPluginContext'; import { ConfigSchema } from '../new-platform/plugin'; +import { createCallApmApi } from '../services/rest/createCallApmApi'; export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { @@ -118,6 +119,7 @@ interface MockSetup { 'apm_oss.transactionIndices': string; 'apm_oss.metricsIndices': string; apmAgentConfigurationIndex: string; + apmCustomLinkIndex: string; }; } @@ -162,7 +164,8 @@ export async function inspectSearchParams( 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex' + apmAgentConfigurationIndex: 'myIndex', + apmCustomLinkIndex: 'myIndex' }, dynamicIndexPattern: null as any }; @@ -195,7 +198,8 @@ const mockCore = { }, notifications: { toasts: { - addWarning: () => {} + addWarning: () => {}, + addDanger: () => {} } } }; @@ -222,6 +226,9 @@ export function MockApmPluginContextWrapper({ children?: ReactNode; value?: ApmPluginContextValue; }) { + if (value.core?.http) { + createCallApmApi(value.core?.http); + } return ( <ApmPluginContext.Provider value={{ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts index cf0c76be4580d..63dbae55790a3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts @@ -5,7 +5,7 @@ */ jest.mock('ui/new_platform'); import { savedMap } from './saved_map'; -import { getQueryFilters } from '../../../server/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; const filterContext = { and: [ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index bc30ca858bd50..78240eee7ce13 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -7,7 +7,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { TimeRange } from 'src/plugins/data/public'; import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { getQueryFilters } from '../../../server/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts index 294d6124c7e33..67356dae5b3e3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts @@ -5,7 +5,7 @@ */ jest.mock('ui/new_platform'); import { savedSearch } from './saved_search'; -import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; const filterContext = { and: [ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts index a351bcb46cdd3..87dc7eb5e814c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts @@ -12,7 +12,7 @@ import { EmbeddableExpression, } from '../../expression_types'; -import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts index 49b4b77de763b..9c3e80bc22af1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts @@ -5,7 +5,7 @@ */ jest.mock('ui/new_platform'); import { savedVisualization } from './saved_visualization'; -import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; const filterContext = { and: [ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index 0315a1f480911..5b612b7cbd666 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -11,7 +11,7 @@ import { EmbeddableExpressionType, EmbeddableExpression, } from '../../expression_types'; -import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index ff22d68772efe..9bdc8e6308e07 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -23,7 +23,7 @@ export const renderApp = ( canvasStore: Store ) => { ReactDOM.render( - <KibanaContextProvider services={{ ...coreStart, ...plugins }}> + <KibanaContextProvider services={{ ...plugins, ...coreStart }}> <I18nProvider> <Provider store={canvasStore}> <App /> diff --git a/x-pack/legacy/plugins/canvas/public/lib/build_bool_array.js b/x-pack/legacy/plugins/canvas/public/lib/build_bool_array.js new file mode 100644 index 0000000000000..2dc6447753526 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/build_bool_array.js @@ -0,0 +1,21 @@ +/* + * 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 { getESFilter } from './get_es_filter'; + +const compact = arr => (Array.isArray(arr) ? arr.filter(val => Boolean(val)) : []); + +export function buildBoolArray(canvasQueryFilterArray) { + return compact( + canvasQueryFilterArray.map(clause => { + try { + return getESFilter(clause); + } catch (e) { + return; + } + }) + ); +} diff --git a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts rename to x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts diff --git a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts similarity index 73% rename from x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts rename to x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts index 05d4c6570bcfb..1a5d2119a94b6 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts @@ -7,17 +7,11 @@ import { Filter } from '../../types'; // @ts-ignore Untyped Local import { buildBoolArray } from './build_bool_array'; - -// TODO: We should be importing from `data/server` below instead of `data/common`, but -// need to keep `data/common` since the contents of this file are currently imported -// by the browser. This file should probably be refactored so that the pieces required -// on the client live in a `public` directory instead. See kibana/issues/52343 -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TimeRange, esFilters, Filter as DataFilter, -} from '../../../../../../src/plugins/data/server'; +} from '../../../../../../src/plugins/data/public'; export interface EmbeddableFilterInput { filters: DataFilter[]; diff --git a/x-pack/legacy/plugins/canvas/public/lib/filters.js b/x-pack/legacy/plugins/canvas/public/lib/filters.js new file mode 100644 index 0000000000000..afa58c7ee30c2 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/filters.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + TODO: This could be pluggable +*/ + +export function time(filter) { + if (!filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter) { + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter) { + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/canvas/public/lib/get_es_filter.js b/x-pack/legacy/plugins/canvas/public/lib/get_es_filter.js new file mode 100644 index 0000000000000..e8a4d704118e8 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/get_es_filter.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + boolArray is the array of bool filter clauses to push filters into. Usually this would be + the value of must, should or must_not. + filter is the abstracted canvas filter. +*/ + +/*eslint import/namespace: ['error', { allowComputed: true }]*/ +import * as filters from './filters'; + +export function getESFilter(filter) { + if (!filters[filter.type]) { + throw new Error(`Unknown filter type: ${filter.type}`); + } + + try { + return filters[filter.type](filter); + } catch (e) { + throw new Error(`Could not create elasticsearch filter from ${filter.type}`); + } +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 0e256d0ab181b..4736dd75831e4 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -24,11 +24,10 @@ import { FrameLayout } from './frame_layout'; // calling this function will wait for all pending Promises from mock // datasources to be processed by its callers. -async function waitForPromises(n = 3) { - for (let i = 0; i < n; ++i) { - await Promise.resolve(); - } -} +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -102,7 +101,7 @@ describe('editor_frame', () => { }); describe('initialization', () => { - it('should initialize initial datasource', () => { + it('should initialize initial datasource', async () => { act(() => { mount( <EditorFrame @@ -119,6 +118,7 @@ describe('editor_frame', () => { /> ); }); + await waitForPromises(); expect(mockDatasource.initialize).toHaveBeenCalled(); }); @@ -145,7 +145,7 @@ describe('editor_frame', () => { expect(mockDatasource.initialize).not.toHaveBeenCalled(); }); - it('should initialize all datasources with state from doc', () => { + it('should initialize all datasources with state from doc', async () => { const mockDatasource3 = createMockDatasource(); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; @@ -185,13 +185,13 @@ describe('editor_frame', () => { /> ); }); - + await waitForPromises(); expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State); expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State); expect(mockDatasource3.initialize).not.toHaveBeenCalled(); }); - it('should not render something before all datasources are initialized', () => { + it('should not render something before all datasources are initialized', async () => { act(() => { mount( <EditorFrame @@ -211,6 +211,7 @@ describe('editor_frame', () => { expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); + await waitForPromises(); }); it('should not initialize visualization before datasource is initialized', async () => { @@ -294,7 +295,9 @@ describe('editor_frame', () => { await waitForPromises(); - mockVisualization.initialize.mock.calls[0][0].addNewLayer(); + act(() => { + mockVisualization.initialize.mock.calls[0][0].addNewLayer(); + }); expect(mockDatasource2.insertLayer).toHaveBeenCalledWith(initialState, expect.anything()); }); @@ -325,7 +328,9 @@ describe('editor_frame', () => { await waitForPromises(); - mockVisualization.initialize.mock.calls[0][0].removeLayers(['abc', 'def']); + act(() => { + mockVisualization.initialize.mock.calls[0][0].removeLayers(['abc', 'def']); + }); expect(mockDatasource2.removeLayer).toHaveBeenCalledWith(initialState, 'abc'); expect(mockDatasource2.removeLayer).toHaveBeenCalledWith({ removed: true }, 'def'); @@ -989,6 +994,7 @@ describe('editor_frame', () => { '[data-test-subj="datasource-switch-testDatasource2"]' ) as HTMLButtonElement).click(); }); + await waitForPromises(); expect(mockDatasource2.initialize).toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 929b4667aeb66..92a14963ff0b6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { ReactExpressionRendererProps } from '../../../../../../../src/plugins/expressions/public'; import { FramePublicAPI, TableSuggestion, Visualization } from '../../types'; import { @@ -22,7 +23,10 @@ import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); describe('workspace_panel', () => { let mockVisualization: jest.Mocked<Visualization>; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 46a8304cc395e..1a38ffa44f6f2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -16,7 +16,10 @@ import { IndexPattern } from './types'; jest.mock('ui/new_platform'); -const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 4c956bfabecc9..18545f31f03c7 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -25,9 +25,6 @@ export interface MlDependencies extends AppMountParameters { data: DataPublicPluginStart; security: SecurityPluginSetup; licensing: LicensingPluginSetup; - __LEGACY: { - XSRF: string; - }; } interface AppProps { @@ -49,7 +46,6 @@ const App: FC<AppProps> = ({ coreStart, deps }) => { recentlyAccessed: coreStart.chrome!.recentlyAccessed, basePath: coreStart.http.basePath, savedObjectsClient: coreStart.savedObjects.client, - XSRF: deps.__LEGACY.XSRF, application: coreStart.application, http: coreStart.http, security: deps.security, diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 23a40d9ecf295..0c6c959927140 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -281,7 +281,7 @@ export function getColumns( defaultMessage: 'actions', }), render: item => { - if (showLinksMenuForItem(item) === true) { + if (showLinksMenuForItem(item, showViewSeriesLink) === true) { return ( <LinksMenu anomaly={item} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js index 89589c98b52c2..32b5634b143db 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js @@ -39,8 +39,7 @@ function randomNumber(min, max) { } function saveWatch(watchModel) { - const basePath = getBasePath(); - const path = basePath.prepend('/api/watcher'); + const path = '/api/watcher'; const url = `${path}/watch/${watchModel.id}`; return http({ @@ -188,8 +187,7 @@ class CreateWatchService { loadWatch(jobId) { const id = `ml-${jobId}`; - const basePath = getBasePath(); - const path = basePath.prepend('/api/watcher'); + const path = '/api/watcher'; const url = `${path}/watch/${id}`; return http({ url, diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index a6d1bbfcee9f6..99a2e8353a874 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -54,7 +54,6 @@ function initManagementSection() { setDependencyCache({ docLinks: legacyDocLinks as any, basePath: legacyBasePath as any, - XSRF: chrome.getXsrfToken(), }); management.register('ml', { diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts index 73a30dbcd71b2..75db2470d77cc 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -4,68 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -// service for interacting with the server +import { Observable } from 'rxjs'; -import { fromFetch } from 'rxjs/fetch'; -import { from, Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; - -import { getXSRF } from '../util/dependency_cache'; - -export interface HttpOptions { - url?: string; -} +import { getHttp } from '../util/dependency_cache'; function getResultHeaders(headers: HeadersInit): HeadersInit { return { - asSystemRequest: false, + asSystemRequest: true, 'Content-Type': 'application/json', - 'kbn-version': getXSRF(), ...headers, } as HeadersInit; } -export function http(options: any) { - return new Promise((resolve, reject) => { - if (options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = getResultHeaders(options.headers ?? {}); - - const allHeaders = - options.headers === undefined ? headers : { ...options.headers, ...headers }; - const body = options.data === undefined ? null : JSON.stringify(options.data); - - const payload: RequestInit = { - method: options.method || 'GET', - headers: allHeaders, - credentials: 'same-origin', - }; - - if (body !== null) { - payload.body = body; - } +interface HttpOptions { + url: string; + method: string; + headers?: any; + data?: any; +} - fetch(url, payload) - .then(resp => { - resp - .json() - .then(resp.ok === true ? resolve : reject) - .catch(resp.ok === true ? resolve : reject); - }) - .catch(resp => { - reject(resp); - }); - } else { - reject(); +/** + * Function for making HTTP requests to Kibana's backend. + * Wrapper for Kibana's HttpHandler. + */ +export async function http(options: HttpOptions) { + if (!options?.url) { + throw new Error('URL is missing'); + } + + try { + let url = ''; + url = url + (options.url || ''); + const headers = getResultHeaders(options.headers ?? {}); + + const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: RequestInit = { + method: options.method || 'GET', + headers: allHeaders, + credentials: 'same-origin', + }; + + if (body !== null) { + payload.body = body; } - }); + + return await getHttp().fetch(url, payload); + } catch (e) { + throw new Error(e); + } } interface RequestOptions extends RequestInit { body: BodyInit | any; } +/** + * Function for making HTTP requests to Kibana's backend which returns an Observable + * with request cancellation support. + */ export function http$<T>(url: string, options: RequestOptions): Observable<T> { const requestInit: RequestInit = { ...options, @@ -75,13 +73,56 @@ export function http$<T>(url: string, options: RequestOptions): Observable<T> { headers: getResultHeaders(options.headers ?? {}), }; - return fromFetch(url, requestInit).pipe( - switchMap(response => { - if (response.ok) { - return from(response.json() as Promise<T>); + return fromHttpHandler<T>(url, requestInit); +} + +/** + * Creates an Observable from Kibana's HttpHandler. + */ +export function fromHttpHandler<T>(input: string, init?: RequestInit): Observable<T> { + return new Observable<T>(subscriber => { + const controller = new AbortController(); + const signal = controller.signal; + + let abortable = true; + let unsubscribed = false; + + if (init?.signal) { + if (init.signal.aborted) { + controller.abort(); } else { - throw new Error(String(response.status)); + init.signal.addEventListener('abort', () => { + if (!signal.aborted) { + controller.abort(); + } + }); } - }) - ); + } + + const perSubscriberInit: RequestInit = { + ...(init ? init : {}), + signal, + }; + + getHttp() + .fetch<T>(input, perSubscriberInit) + .then(response => { + abortable = false; + subscriber.next(response); + subscriber.complete(); + }) + .catch(err => { + abortable = false; + if (!unsubscribed) { + subscriber.error(err); + } + }); + + return () => { + unsubscribed = true; + if (abortable) { + controller.abort(); + } + }; + }); } diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index 6fdc76d7244d3..688abd1383ecb 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -13,10 +13,9 @@ import { filters } from './filters'; import { results } from './results'; import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; -import { getBasePath } from '../../util/dependency_cache'; export function basePath() { - return getBasePath().prepend('/api/ml'); + return '/api/ml'; } export const ml = { @@ -452,7 +451,7 @@ export const ml = { }, getIndices() { - const tempBasePath = getBasePath().prepend('/api'); + const tempBasePath = '/api'; return http({ url: `${tempBasePath}/index_management/indices`, method: 'GET', diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index c167d7e7c3d42..2a1ffe79d033c 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -35,7 +35,6 @@ export interface DependencyCache { autocomplete: DataPublicPluginStart['autocomplete'] | null; basePath: IBasePath | null; savedObjectsClient: SavedObjectsClientContract | null; - XSRF: string | null; application: ApplicationStart | null; http: HttpStart | null; security: SecurityPluginSetup | null; @@ -54,7 +53,6 @@ const cache: DependencyCache = { autocomplete: null, basePath: null, savedObjectsClient: null, - XSRF: null, application: null, http: null, security: null, @@ -73,7 +71,6 @@ export function setDependencyCache(deps: Partial<DependencyCache>) { cache.autocomplete = deps.autocomplete || null; cache.basePath = deps.basePath || null; cache.savedObjectsClient = deps.savedObjectsClient || null; - cache.XSRF = deps.XSRF || null; cache.application = deps.application || null; cache.http = deps.http || null; cache.security = deps.security || null; @@ -162,13 +159,6 @@ export function getSavedObjectsClient() { return cache.savedObjectsClient; } -export function getXSRF() { - if (cache.XSRF === null) { - throw new Error("xsrf hasn't been initialized"); - } - return cache.XSRF; -} - export function getApplication() { if (cache.application === null) { throw new Error("application hasn't been initialized"); diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index 0c6c0bd8dd29e..9fb53e78d9454 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; import { PluginInitializerContext } from 'src/core/public'; import { SecurityPluginSetup } from '../../../../plugins/security/public'; @@ -26,8 +25,5 @@ export const setup = pluginInstance.setup(npSetup.core, { data: npStart.plugins.data, security: setupDependencies.security, licensing: setupDependencies.licensing, - __LEGACY: { - XSRF: chrome.getXsrfToken(), - }, }); export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index c0369a74c070a..7b3a5f6fadfac 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -8,7 +8,7 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; import { MlDependencies } from './application/app'; export class MlPlugin implements Plugin<Setup, Start> { - setup(core: CoreSetup, { data, security, licensing, __LEGACY }: MlDependencies) { + setup(core: CoreSetup, { data, security, licensing }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -21,7 +21,6 @@ export class MlPlugin implements Plugin<Setup, Start> { onAppLeave: params.onAppLeave, history: params.history, data, - __LEGACY, security, licensing, }); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 1a57408f41dd6..f90f2c7aee395 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -3,27 +3,28 @@ * 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 del from 'del'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { Browser, - Page, - LaunchOptions, ConsoleMessage, + LaunchOptions, + Page, Request as PuppeteerRequest, } from 'puppeteer'; -import del from 'del'; import * as Rx from 'rxjs'; -import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; - +import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { BrowserConfig, CaptureConfig } from '../../../../types'; import { LevelLogger as Logger } from '../../../lib/level_logger'; -import { HeadlessChromiumDriver } from '../driver'; import { safeChildProcess } from '../../safe_child_process'; -import { puppeteerLaunch } from '../puppeteer'; +import { HeadlessChromiumDriver } from '../driver'; import { getChromeLogLocation } from '../paths'; +import { puppeteerLaunch } from '../puppeteer'; import { args } from './args'; type binaryPath = string; @@ -167,7 +168,7 @@ export class HeadlessChromiumDriverFactory { logger.debug(`deleting chromium user data directory at [${userDataDir}]`); // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. - del(userDataDir).catch(error => { + del(userDataDir, { force: true }).catch(error => { logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); }); }); @@ -216,17 +217,35 @@ export class HeadlessChromiumDriverFactory { } getPageExit(browser: Browser, page: Page) { - const pageError$ = Rx.fromEvent<Error>(page, 'error').pipe(mergeMap(err => Rx.throwError(err))); + const pageError$ = Rx.fromEvent<Error>(page, 'error').pipe( + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { + defaultMessage: 'Reporting detected an error: {err}', + values: { err: err.toString() }, + }) + ); + }) + ); const uncaughtExceptionPageError$ = Rx.fromEvent<Error>(page, 'pageerror').pipe( - mergeMap(err => Rx.throwError(err)) + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { + defaultMessage: `Reporting detected an error on the page: {err}`, + values: { err: err.toString() }, + }) + ); + }) ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( mergeMap(() => Rx.throwError( new Error( - `Puppeteer was disconnected from the Chromium instance! Chromium has closed or crashed.` + i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { + defaultMessage: `Reporting detected that Chromium has closed.`, + }) ) ) ) diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts index 4355a6a0a1773..a2d1fc7f91a29 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts @@ -31,7 +31,7 @@ export async function clean(dir: string, expectedPaths: string[]) { const path = resolvePath(dir, filename); if (!expectedPaths.includes(path)) { log(`Deleting unexpected file ${path}`); - await del(path); + await del(path, { force: true }); } }); } diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts new file mode 100644 index 0000000000000..f2ed9d48daaf6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ELASTIC_RULES_BTN, RULES_TABLE, RULES_ROW } from '../screens/signal_detection_rules'; + +import { + changeToThreeHundredRowsPerPage, + loadPrebuiltDetectionRules, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForPrebuiltDetectionRulesToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/signal_detection_rules'; +import { + goToManageSignalDetectionRules, + waitForSignalsIndexToBeCreated, + waitForSignalsPanelToBeLoaded, +} from '../tasks/detections'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Signal detection rules', () => { + before(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS); + }); + it('Loads prebuilt rules', () => { + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); + goToManageSignalDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + loadPrebuiltDetectionRules(); + waitForPrebuiltDetectionRulesToBeLoaded(); + + const expectedElasticRulesBtnText = 'Elastic rules (92)'; + cy.get(ELASTIC_RULES_BTN) + .invoke('text') + .should('eql', expectedElasticRulesBtnText); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 92; + cy.get(RULES_TABLE).then($table => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts new file mode 100644 index 0000000000000..8089b028a10d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; + +export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts new file mode 100644 index 0000000000000..bfaa86e83f301 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]'; + +export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; + +export const LOADING_INITIAL_PREBUILT_RULES_TABLE = + '[data-test-subj="initialLoadingPanelAllRulesTable"]'; + +export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]'; + +export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; + +export const RULES_TABLE = '[data-test-subj="rules-table"]'; + +export const RULES_ROW = '.euiTableRow'; + +export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts new file mode 100644 index 0000000000000..4a0a565a74e27 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -0,0 +1,28 @@ +/* + * 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 { LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN } from '../screens/detections'; + +export const goToManageSignalDetectionRules = () => { + cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForSignalsIndexToBeCreated = () => { + cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( + response => { + if (response.status !== 200) { + cy.wait(7500); + } + } + ); +}; + +export const waitForSignalsPanelToBeLoaded = () => { + cy.get(LOADING_SIGNALS_PANEL).should('exist'); + cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts new file mode 100644 index 0000000000000..cc0e4bce1035a --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts @@ -0,0 +1,40 @@ +/* + * 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 { + LOAD_PREBUILT_RULES_BTN, + LOADING_INITIAL_PREBUILT_RULES_TABLE, + LOADING_SPINNER, + PAGINATION_POPOVER_BTN, + RULES_TABLE, + THREE_HUNDRED_ROWS, +} from '../screens/signal_detection_rules'; + +export const changeToThreeHundredRowsPerPage = () => { + cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); + cy.get(THREE_HUNDRED_ROWS).click(); +}; + +export const loadPrebuiltDetectionRules = () => { + cy.get(LOAD_PREBUILT_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded = () => { + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('exist'); + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('not.exist'); +}; + +export const waitForPrebuiltDetectionRulesToBeLoaded = () => { + cy.get(LOAD_PREBUILT_RULES_BTN).should('not.exist'); + cy.get(RULES_TABLE).should('exist'); +}; + +export const waitForRulesToBeLoaded = () => { + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 8fdc939e7ee51..5e65e5aa34c18 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index fb977417ffbbf..38ec4a4b6f1f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -49,7 +49,7 @@ const EuiFlyoutContainer = styled.div<{ headerHeight: number }>` .timeline-flyout-body { overflow-y: hidden; padding: 0; - .euiFlyoutBody__overflow { + .euiFlyoutBody__overflowContent { padding: 0; } } diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 0885f15b1efba..ad2d57b948ba0 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -16,6 +16,7 @@ exports[`rendering renders correctly 1`] = ` grow={false} > <EuiLoadingSpinner + data-test-subj="loading-spinner" size="xl" /> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index be2ce3dde951c..e78f148418588 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -62,7 +62,7 @@ export const Loader = React.memo<LoaderProps>(({ children, overlay, overlayBackg <Aside overlay={overlay} overlayBackground={overlayBackground}> <FlexGroup overlay={{ overlay }}> <EuiFlexItem grow={false}> - <EuiLoadingSpinner size={size} /> + <EuiLoadingSpinner data-test-subj="loading-spinner" size={size} /> </EuiFlexItem> {children && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index 75f19218d9b38..afd325f539966 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -290,7 +290,7 @@ const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ return ( <EuiPanel> <HeaderSection title={i18n.SIGNALS_TABLE_TITLE} /> - <EuiLoadingContent /> + <EuiLoadingContent data-test-subj="loading-signals-panel" /> </EuiPanel> ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index c3fb907ae83e1..1bd7ab2c4f1ae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -129,7 +129,12 @@ const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({ } title={i18n.PAGE_TITLE} > - <EuiButton fill href={getRulesUrl()} iconType="gear"> + <EuiButton + fill + href={getRulesUrl()} + iconType="gear" + data-test-subj="manage-signal-detection-rules" + > {i18n.BUTTON_MANAGE_RULES} </EuiButton> </DetectionEngineHeaderPage> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index e7d68164c4ef4..bb718d8029817 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -317,6 +317,7 @@ export const AllRules = React.memo<AllRulesProps>( </UtilityBarSection> </UtilityBar> <MyEuiBasicTable + data-test-subj="rules-table" columns={columns} isSelectable={!hasNoPermissions ?? false} itemId="id" diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx index 41b7fafd6becd..1cff4751e8188 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx @@ -43,6 +43,7 @@ const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = ( isDisabled={userHasNoPermissions} isLoading={loading} onClick={handlePreBuiltCreation} + data-test-subj="load-prebuilt-rules" > {i18n.PRE_BUILT_ACTION} </EuiButton> diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 1d904b2b349ae..5fdef59a72f04 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -43,6 +43,7 @@ export const patchRules = async ({ type, references, version, + throttle, }: PatchRuleParams): Promise<PartialAlert | null> => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -73,6 +74,7 @@ export const patchRules = async ({ type, references, version, + throttle, }); const nextParams = defaults( @@ -108,6 +110,7 @@ export const patchRules = async ({ id: rule.id, data: { tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), + throttle: throttle ?? rule.throttle ?? null, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index c63237c93daf4..7889267a7267b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -41,6 +41,7 @@ export const updatePrepackagedRules = async ( threat, references, version, + throttle, } = rule; // Note: we do not pass down enabled as we do not want to suddenly disable @@ -73,6 +74,7 @@ export const updatePrepackagedRules = async ( threat, references, version, + throttle, }); }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 9ead8313b2c91..3a10841b70d7e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -42,6 +42,7 @@ export const updateRules = async ({ type, references, version, + throttle, }: UpdateRuleParams): Promise<PartialAlert | null> => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -72,6 +73,7 @@ export const updateRules = async ({ type, references, version, + throttle, }); const update = await alertsClient.update({ @@ -81,6 +83,7 @@ export const updateRules = async ({ name, schedule: { interval }, actions: rule.actions, + throttle: throttle ?? rule.throttle ?? null, params: { description, ruleId: rule.params.ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 77eefd3d1d855..5e5ff157c92c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -49,6 +49,7 @@ export interface RuleAlertParams { threat: ThreatParams[] | undefined | null; type: 'query' | 'saved_query'; version: number; + throttle?: string; } export type RuleTypeParams = Omit<RuleAlertParams, 'name' | 'enabled' | 'interval' | 'tags'>; diff --git a/x-pack/legacy/plugins/transform/index.ts b/x-pack/legacy/plugins/transform/index.ts index 10f4732152c43..a4b980c0bf8f3 100644 --- a/x-pack/legacy/plugins/transform/index.ts +++ b/x-pack/legacy/plugins/transform/index.ts @@ -4,19 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; - -import { PLUGIN } from './common/constants'; - export function transform(kibana: any) { return new kibana.Plugin({ - id: PLUGIN.ID, + id: 'transform', configPrefix: 'xpack.transform', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/app/index.scss'), - managementSections: ['plugins/transform'], - }, }); } diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts deleted file mode 100644 index 802599aaedd4f..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts +++ /dev/null @@ -1,103 +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 { useAppDependencies } from '../app_dependencies'; -import { PreviewRequestBody, TransformId } from '../common'; -import { httpFactory, Http } from '../services/http_service'; - -import { EsIndex, TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; - -const apiFactory = (basePath: string, indicesBasePath: string, http: Http) => ({ - getTransforms(transformId?: TransformId): Promise<any> { - const transformIdString = transformId !== undefined ? `/${transformId}` : ''; - return http({ - url: `${basePath}/transforms${transformIdString}`, - method: 'GET', - }); - }, - getTransformsStats(transformId?: TransformId): Promise<any> { - if (transformId !== undefined) { - return http({ - url: `${basePath}/transforms/${transformId}/_stats`, - method: 'GET', - }); - } - - return http({ - url: `${basePath}/transforms/_stats`, - method: 'GET', - }); - }, - createTransform(transformId: TransformId, transformConfig: any): Promise<any> { - return http({ - url: `${basePath}/transforms/${transformId}`, - method: 'PUT', - data: transformConfig, - }); - }, - deleteTransforms(transformsInfo: TransformEndpointRequest[]) { - return http({ - url: `${basePath}/delete_transforms`, - method: 'POST', - data: transformsInfo, - }) as Promise<TransformEndpointResult>; - }, - getTransformsPreview(obj: PreviewRequestBody): Promise<any> { - return http({ - url: `${basePath}/transforms/_preview`, - method: 'POST', - data: obj, - }); - }, - startTransforms(transformsInfo: TransformEndpointRequest[]) { - return http({ - url: `${basePath}/start_transforms`, - method: 'POST', - data: { - transformsInfo, - }, - }) as Promise<TransformEndpointResult>; - }, - stopTransforms(transformsInfo: TransformEndpointRequest[]) { - return http({ - url: `${basePath}/stop_transforms`, - method: 'POST', - data: { - transformsInfo, - }, - }) as Promise<TransformEndpointResult>; - }, - getTransformAuditMessages(transformId: TransformId): Promise<any> { - return http({ - url: `${basePath}/transforms/${transformId}/messages`, - method: 'GET', - }); - }, - esSearch(payload: any) { - return http({ - url: `${basePath}/es_search`, - method: 'POST', - data: payload, - }) as Promise<any>; - }, - getIndices() { - return http({ - url: `${indicesBasePath}/index_management/indices`, - method: 'GET', - }) as Promise<EsIndex[]>; - }, -}); - -export const useApi = () => { - const appDeps = useAppDependencies(); - - const basePath = appDeps.core.http.basePath.prepend('/api/transform'); - const indicesBasePath = appDeps.core.http.basePath.prepend('/api'); - const xsrfToken = appDeps.plugins.xsrfToken; - const http = httpFactory(xsrfToken); - - return apiFactory(basePath, indicesBasePath, http); -}; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_api_types.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_api_types.ts deleted file mode 100644 index d0f81a058b7b3..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_api_types.ts +++ /dev/null @@ -1,25 +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 { TransformId, TRANSFORM_STATE } from '../common'; - -export interface EsIndex { - name: string; -} - -export interface TransformEndpointRequest { - id: TransformId; - state?: TRANSFORM_STATE; -} - -export interface ResultData { - success: boolean; - error?: any; -} - -export interface TransformEndpointResult { - [key: string]: ResultData; -} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx deleted file mode 100644 index 715573e3a6f67..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ /dev/null @@ -1,66 +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, { FC } from 'react'; -import ReactDOM from 'react-dom'; -import { act } from 'react-dom/test-utils'; - -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; - -jest.mock('../../../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC<TestHookProps> = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - act(() => { - ReactDOM.render(<TestHook callback={callback} />, container); - }); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let sourceIndexObj: UseSourceIndexDataReturnType; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', () => { - testHook(() => { - act(() => { - sourceIndexObj = useSourceIndexData( - { id: 'the-id', title: 'the-title', fields: [] }, - query, - { pageIndex: 0, pageSize: 10 } - ); - }); - }); - - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts b/x-pack/legacy/plugins/transform/public/app/services/http_service.ts deleted file mode 100644 index fa4c8d1ba7844..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// service for interacting with the server -import { Dictionary } from '../../../common/types/common'; - -export type Http = (options: Dictionary<any>) => Promise<unknown>; - -export function httpFactory(xsrfToken: string) { - return function http(options: Dictionary<any>) { - return new Promise((resolve, reject) => { - if (options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = { - 'kbn-system-request': true, - 'Content-Type': 'application/json', - 'kbn-version': xsrfToken, - ...options.headers, - }; - - const allHeaders = - options.headers === undefined ? headers : { ...options.headers, ...headers }; - const body = options.data === undefined ? null : JSON.stringify(options.data); - - const payload: Dictionary<any> = { - method: options.method || 'GET', - headers: allHeaders, - credentials: 'same-origin', - }; - - if (body !== null) { - payload.body = body; - } - - fetch(url, payload) - .then(resp => { - resp.json().then(resp.ok === true ? resolve : reject); - }) - .catch(resp => { - reject(resp); - }); - } else { - reject(); - } - }); - }; -} diff --git a/x-pack/legacy/plugins/transform/public/app/services/ui_metric/ui_metric.ts b/x-pack/legacy/plugins/transform/public/app/services/ui_metric/ui_metric.ts deleted file mode 100644 index a2f0a6e1a5482..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/services/ui_metric/ui_metric.ts +++ /dev/null @@ -1,25 +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 { UIM_APP_NAME } from '../../constants'; -import { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; - -class UiMetricService { - track?: ReturnType<typeof createUiStatsReporter>; - - public init = (getReporter: typeof createUiStatsReporter): void => { - this.track = getReporter(UIM_APP_NAME); - }; - - public trackUiMetric = (eventName: string): void => { - if (!this.track) throw Error('UiMetricService not initialized.'); - return this.track(METRIC_TYPE.COUNT, eventName); - }; -} - -export const uiMetricService = new UiMetricService(); diff --git a/x-pack/legacy/plugins/transform/public/plugin.ts b/x-pack/legacy/plugins/transform/public/plugin.ts deleted file mode 100644 index 7b5fbbb4a2151..0000000000000 --- a/x-pack/legacy/plugins/transform/public/plugin.ts +++ /dev/null @@ -1,81 +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 { renderApp } from './app/app'; -import { ShimCore, ShimPlugins } from './shim'; - -import { breadcrumbService } from './app/services/navigation'; -import { docTitleService } from './app/services/navigation'; -import { textService } from './app/services/text'; -import { uiMetricService } from './app/services/ui_metric'; - -export class Plugin { - public start(core: ShimCore, plugins: ShimPlugins): void { - const { - http, - chrome, - documentation, - docLinks, - docTitle, - injectedMetadata, - notifications, - uiSettings, - savedObjects, - overlays, - } = core; - const { data, management, uiMetric, xsrfToken } = plugins; - - // AppCore/AppPlugins to be passed on as React context - const appDependencies = { - core: { - chrome, - documentation, - docLinks, - http, - i18n: core.i18n, - injectedMetadata, - notifications, - uiSettings, - savedObjects, - overlays, - }, - plugins: { - data, - management, - xsrfToken, - }, - }; - - // Register management section - const esSection = management.sections.getSection('elasticsearch'); - if (esSection !== undefined) { - esSection.registerApp({ - id: 'transform', - title: i18n.translate('xpack.transform.appTitle', { - defaultMessage: 'Transforms', - }), - order: 3, - mount(params) { - breadcrumbService.setup(params.setBreadcrumbs); - params.setBreadcrumbs([ - { - text: i18n.translate('xpack.transform.breadcrumbsTitle', { - defaultMessage: 'Transforms', - }), - }, - ]); - - return renderApp(params.element, appDependencies); - }, - }); - } - - // Initialize services - textService.init(); - uiMetricService.init(uiMetric.createUiStatsReporter); - docTitleService.init(docTitle.change); - } -} diff --git a/x-pack/legacy/plugins/transform/public/shim.ts b/x-pack/legacy/plugins/transform/public/shim.ts deleted file mode 100644 index 9941aabcf3255..0000000000000 --- a/x-pack/legacy/plugins/transform/public/shim.ts +++ /dev/null @@ -1,94 +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 { npStart } from 'ui/new_platform'; - -import chrome from 'ui/chrome'; -import { docTitle } from 'ui/doc_title/doc_title'; - -// @ts-ignore: allow traversal to fail on x-pack build -import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; - -import { TRANSFORM_DOC_PATHS } from './app/constants'; - -export type NpCore = typeof npStart.core; -export type NpPlugins = typeof npStart.plugins; - -// AppCore/AppPlugins is the set of core features/plugins -// we pass on via context/hooks to the app and its components. -export type AppCore = Pick< - ShimCore, - | 'chrome' - | 'documentation' - | 'docLinks' - | 'http' - | 'i18n' - | 'injectedMetadata' - | 'savedObjects' - | 'uiSettings' - | 'overlays' - | 'notifications' ->; -export type AppPlugins = Pick<ShimPlugins, 'data' | 'management' | 'xsrfToken'>; - -export interface AppDependencies { - core: AppCore; - plugins: AppPlugins; -} - -export interface ShimCore extends NpCore { - documentation: Record< - | 'esDocBasePath' - | 'esIndicesCreateIndex' - | 'esPluginDocBasePath' - | 'esQueryDsl' - | 'esStackOverviewDocBasePath' - | 'esTransform' - | 'esTransformPivot' - | 'mlDocBasePath', - string - >; - docTitle: { - change: typeof docTitle.change; - }; -} - -export interface ShimPlugins extends NpPlugins { - uiMetric: { - createUiStatsReporter: typeof createUiStatsReporter; - }; - xsrfToken: string; -} - -export function createPublicShim(): { core: ShimCore; plugins: ShimPlugins } { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = npStart.core.docLinks; - - return { - core: { - ...npStart.core, - documentation: { - esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`, - esIndicesCreateIndex: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/indices-create-index.html#indices-create-index`, - esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, - esQueryDsl: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl.html`, - esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`, - esTransform: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/${TRANSFORM_DOC_PATHS.transforms}`, - esTransformPivot: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/put-transform.html#put-transform-request-body`, - mlDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, - }, - docTitle: { - change: docTitle.change, - }, - }, - plugins: { - ...npStart.plugins, - uiMetric: { - createUiStatsReporter, - }, - xsrfToken: chrome.getXsrfToken(), - }, - }; -} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss b/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss index 3ae0ef35ee354..8c83c0a571f28 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss +++ b/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss @@ -1,8 +1,2 @@ // Imported EUI @import 'src/legacy/ui/public/styles/_styling_constants'; - -// Styling within the app -@import '../../../../plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/index'; - -@import '../../../../plugins/triggers_actions_ui/public/application/sections/action_connector_form/index'; - diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 646ea168b52a5..0be1983477256 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -76,7 +76,7 @@ describe('config validation', () => { }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [index]: types that failed validation: - [index.0]: expected value of type [string] but got [number] -- [index.1]: expected value to equal [null] but got [666]" +- [index.1]: expected value to equal [null]" `); }); }); @@ -150,7 +150,7 @@ describe('params validation', () => { expect(() => { validateParams(actionType, { documents: ['should be an object'] }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [documents.0]: could not parse record value from [should be an object]"` + `"error validating action params: [documents.0]: could not parse record value from json input"` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index ab860e4c3bbba..caa183d665e09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -137,9 +137,9 @@ describe('validateParams()', () => { validateParams(actionType, { eventAction: 'ackynollage' }); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [eventAction]: types that failed validation: -- [eventAction.0]: expected value to equal [trigger] but got [ackynollage] -- [eventAction.1]: expected value to equal [resolve] but got [ackynollage] -- [eventAction.2]: expected value to equal [acknowledge] but got [ackynollage]" +- [eventAction.0]: expected value to equal [trigger] +- [eventAction.1]: expected value to equal [resolve] +- [eventAction.2]: expected value to equal [acknowledge]" `); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index 6a4482f362c2b..bb806f8ae36fc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -63,24 +63,24 @@ describe('validateParams()', () => { validateParams(actionType, { message: 'x', level: 2 }); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [level]: types that failed validation: -- [level.0]: expected value to equal [trace] but got [2] -- [level.1]: expected value to equal [debug] but got [2] -- [level.2]: expected value to equal [info] but got [2] -- [level.3]: expected value to equal [warn] but got [2] -- [level.4]: expected value to equal [error] but got [2] -- [level.5]: expected value to equal [fatal] but got [2]" +- [level.0]: expected value to equal [trace] +- [level.1]: expected value to equal [debug] +- [level.2]: expected value to equal [info] +- [level.3]: expected value to equal [warn] +- [level.4]: expected value to equal [error] +- [level.5]: expected value to equal [fatal]" `); expect(() => { validateParams(actionType, { message: 'x', level: 'foo' }); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [level]: types that failed validation: -- [level.0]: expected value to equal [trace] but got [foo] -- [level.1]: expected value to equal [debug] but got [foo] -- [level.2]: expected value to equal [info] but got [foo] -- [level.3]: expected value to equal [warn] but got [foo] -- [level.4]: expected value to equal [error] but got [foo] -- [level.5]: expected value to equal [fatal] but got [foo]" +- [level.0]: expected value to equal [trace] +- [level.1]: expected value to equal [debug] +- [level.2]: expected value to equal [info] +- [level.3]: expected value to equal [warn] +- [level.4]: expected value to equal [error] +- [level.5]: expected value to equal [fatal]" `); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index e553e5c83712a..d8f75de781841 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -104,8 +104,8 @@ describe('config validation', () => { validateConfig(actionType, config); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [method]: types that failed validation: -- [method.0]: expected value to equal [post] but got [https] -- [method.1]: expected value to equal [put] but got [https]" +- [method.0]: expected value to equal [post] +- [method.1]: expected value to equal [put]" `); }); @@ -141,8 +141,8 @@ describe('config validation', () => { validateConfig(actionType, config); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [headers]: types that failed validation: -- [headers.0]: could not parse record value from [application/json] -- [headers.1]: expected value to equal [null] but got [application/json]" +- [headers.0]: could not parse record value from json input +- [headers.1]: expected value to equal [null]" `); }); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss new file mode 100644 index 0000000000000..2ba6f9baca90d --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss @@ -0,0 +1,10 @@ +.auaActionWizard__selectedActionFactoryContainer { + background-color: $euiColorLightestShade; + padding: $euiSize; +} + +.auaActionWizard__actionFactoryItem { + .euiKeyPadMenuItem__label { + height: #{$euiSizeXL}; + } +} diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx new file mode 100644 index 0000000000000..62f16890cade2 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { dashboardDrilldownActionFactory, Demo, urlDrilldownActionFactory } from './test_data'; + +storiesOf('components/ActionWizard', module) + .add('default', () => ( + <Demo actionFactories={[dashboardDrilldownActionFactory, urlDrilldownActionFactory]} /> + )) + .add('Only one factory is available', () => ( + // to make sure layout doesn't break + <Demo actionFactories={[dashboardDrilldownActionFactory]} /> + )) + .add('Long list of action factories', () => ( + // to make sure layout doesn't break + <Demo + actionFactories={[ + dashboardDrilldownActionFactory, + urlDrilldownActionFactory, + dashboardDrilldownActionFactory, + urlDrilldownActionFactory, + dashboardDrilldownActionFactory, + urlDrilldownActionFactory, + dashboardDrilldownActionFactory, + urlDrilldownActionFactory, + ]} + /> + )); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx new file mode 100644 index 0000000000000..aea47be693b8f --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.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 React from 'react'; +import { cleanup, fireEvent, render } from '@testing-library/react/pure'; +import '@testing-library/jest-dom/extend-expect'; // TODO: this should be global +import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard'; +import { + dashboardDrilldownActionFactory, + dashboards, + Demo, + urlDrilldownActionFactory, +} from './test_data'; + +// TODO: afterEach is not available for it globally during setup +// https://github.com/elastic/kibana/issues/59469 +afterEach(cleanup); + +test('Pick and configure action', () => { + const screen = render( + <Demo actionFactories={[dashboardDrilldownActionFactory, urlDrilldownActionFactory]} /> + ); + + // check that all factories are displayed to pick + expect(screen.getAllByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).toHaveLength(2); + + // select URL one + fireEvent.click(screen.getByText(/Go to URL/i)); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + // change to dashboard + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to Dashboard/i)); + + // Select dashboard + fireEvent.change(screen.getByLabelText(/Choose destination dashboard/i), { + target: { value: dashboards[1].id }, + }); +}); + +test('If only one actions factory is available then actionFactory selection is emitted without user input', () => { + const screen = render(<Demo actionFactories={[urlDrilldownActionFactory]} />); + + // check that no factories are displayed to pick from + expect(screen.queryByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).not.toBeInTheDocument(); + expect(screen.queryByTestId(TEST_SUBJ_SELECTED_ACTION_FACTORY)).toBeInTheDocument(); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + // check that can't change to action factory type + expect(screen.queryByTestId(/change/i)).not.toBeInTheDocument(); +}); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx new file mode 100644 index 0000000000000..41ef863c00e44 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -0,0 +1,196 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + EuiKeyPadMenuItemButton, +} from '@elastic/eui'; +import { txtChangeButton } from './i18n'; +import './action_wizard.scss'; + +// TODO: this interface is temporary for just moving forward with the component +// and it will be imported from the ../ui_actions when implemented properly +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ActionBaseConfig = {}; +export interface ActionFactory<Config extends ActionBaseConfig = ActionBaseConfig> { + type: string; // TODO: type should be tied to Action and ActionByType + displayName: string; + iconType?: string; + wizard: React.FC<ActionFactoryWizardProps<Config>>; + createConfig: () => Config; + isValid: (config: Config) => boolean; +} + +export interface ActionFactoryWizardProps<Config extends ActionBaseConfig> { + config?: Config; + + /** + * Callback called when user updates the config in UI. + */ + onConfig: (config: Config) => void; +} + +export interface ActionWizardProps { + /** + * List of available action factories + */ + actionFactories: Array<ActionFactory<any>>; // any here to be able to pass array of ActionFactory<Config> with different configs + + /** + * Currently selected action factory + * undefined - is allowed and means that non is selected + */ + currentActionFactory?: ActionFactory; + /** + * Action factory selected changed + * null - means user click "change" and removed action factory selection + */ + onActionFactoryChange: (actionFactory: ActionFactory | null) => void; + + /** + * current config for currently selected action factory + */ + config?: ActionBaseConfig; + + /** + * config changed + */ + onConfigChange: (config: ActionBaseConfig) => void; +} + +export const ActionWizard: React.FC<ActionWizardProps> = ({ + currentActionFactory, + actionFactories, + onActionFactoryChange, + onConfigChange, + config, +}) => { + // auto pick action factory if there is only 1 available + if (!currentActionFactory && actionFactories.length === 1) { + onActionFactoryChange(actionFactories[0]); + } + + if (currentActionFactory && config) { + return ( + <SelectedActionFactory + actionFactory={currentActionFactory} + showDeselect={actionFactories.length > 1} + onDeselect={() => { + onActionFactoryChange(null); + }} + config={config} + onConfigChange={newConfig => { + onConfigChange(newConfig); + }} + /> + ); + } + + return ( + <ActionFactorySelector + actionFactories={actionFactories} + onActionFactorySelected={actionFactory => { + onActionFactoryChange(actionFactory); + }} + /> + ); +}; + +interface SelectedActionFactoryProps<Config extends ActionBaseConfig = ActionBaseConfig> { + actionFactory: ActionFactory<Config>; + config: Config; + onConfigChange: (config: Config) => void; + showDeselect: boolean; + onDeselect: () => void; +} + +export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selected-action-factory'; + +const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({ + actionFactory, + onDeselect, + showDeselect, + onConfigChange, + config, +}) => { + return ( + <div + className="auaActionWizard__selectedActionFactoryContainer" + data-test-subj={TEST_SUBJ_SELECTED_ACTION_FACTORY} + data-testid={TEST_SUBJ_SELECTED_ACTION_FACTORY} + > + <header> + <EuiFlexGroup alignItems="center" gutterSize="s"> + {actionFactory.iconType && ( + <EuiFlexItem grow={false}> + <EuiIcon type={actionFactory.iconType} size="m" /> + </EuiFlexItem> + )} + <EuiFlexItem grow={true}> + <EuiText> + <h4>{actionFactory.displayName}</h4> + </EuiText> + </EuiFlexItem> + {showDeselect && ( + <EuiFlexItem grow={false}> + <EuiButtonEmpty size="s" onClick={() => onDeselect()}> + {txtChangeButton} + </EuiButtonEmpty> + </EuiFlexItem> + )} + </EuiFlexGroup> + </header> + <EuiSpacer size="m" /> + <div> + {actionFactory.wizard({ + config, + onConfig: onConfigChange, + })} + </div> + </div> + ); +}; + +interface ActionFactorySelectorProps { + actionFactories: ActionFactory[]; + onActionFactorySelected: (actionFactory: ActionFactory) => void; +} + +export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'action-factory-item'; + +const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({ + actionFactories, + onActionFactorySelected, +}) => { + if (actionFactories.length === 0) { + // this is not user facing, as it would be impossible to get into this state + // just leaving for dev purposes for troubleshooting + return <div>No action factories to pick from</div>; + } + + return ( + <EuiFlexGroup wrap> + {actionFactories.map(actionFactory => ( + <EuiKeyPadMenuItemButton + className="auaActionWizard__actionFactoryItem" + key={actionFactory.type} + label={actionFactory.displayName} + data-testid={TEST_SUBJ_ACTION_FACTORY_ITEM} + data-test-subj={TEST_SUBJ_ACTION_FACTORY_ITEM} + onClick={() => onActionFactorySelected(actionFactory)} + > + {actionFactory.iconType && <EuiIcon type={actionFactory.iconType} size="m" />} + </EuiKeyPadMenuItemButton> + ))} + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts new file mode 100644 index 0000000000000..641f25176264a --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtChangeButton = i18n.translate( + 'xpack.advancedUiActions.components.actionWizard.changeButton', + { + defaultMessage: 'change', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts similarity index 79% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js rename to x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts index 4f2cce5861424..ed224248ec4cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.js +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { TimeBuckets } from './time_buckets'; +export { ActionFactory, ActionWizard } from './action_wizard'; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx new file mode 100644 index 0000000000000..8ecdde681069e --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; +import { ActionFactory, ActionBaseConfig, ActionWizard } from './action_wizard'; + +export const dashboards = [ + { id: 'dashboard1', title: 'Dashboard 1' }, + { id: 'dashboard2', title: 'Dashboard 2' }, +]; + +export const dashboardDrilldownActionFactory: ActionFactory<{ + dashboardId?: string; + useCurrentDashboardFilters: boolean; + useCurrentDashboardDataRange: boolean; +}> = { + type: 'Dashboard', + displayName: 'Go to Dashboard', + iconType: 'dashboardApp', + createConfig: () => { + return { + dashboardId: undefined, + useCurrentDashboardDataRange: true, + useCurrentDashboardFilters: true, + }; + }, + isValid: config => { + if (!config.dashboardId) return false; + return true; + }, + wizard: props => { + const config = props.config ?? { + dashboardId: undefined, + useCurrentDashboardDataRange: true, + useCurrentDashboardFilters: true, + }; + return ( + <> + <EuiFormRow label="Choose destination dashboard:"> + <EuiSelect + name="selectDashboard" + hasNoInitialSelection={true} + options={dashboards.map(({ id, title }) => ({ value: id, text: title }))} + value={config.dashboardId} + onChange={e => { + props.onConfig({ ...config, dashboardId: e.target.value }); + }} + /> + </EuiFormRow> + <EuiFormRow hasChildLabel={false}> + <EuiSwitch + name="useCurrentFilters" + label="Use current dashboard's filters" + checked={config.useCurrentDashboardFilters} + onChange={() => + props.onConfig({ + ...config, + useCurrentDashboardFilters: !config.useCurrentDashboardFilters, + }) + } + /> + </EuiFormRow> + <EuiFormRow hasChildLabel={false}> + <EuiSwitch + name="useCurrentDateRange" + label="Use current dashboard's date range" + checked={config.useCurrentDashboardDataRange} + onChange={() => + props.onConfig({ + ...config, + useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, + }) + } + /> + </EuiFormRow> + </> + ); + }, +}; + +export const urlDrilldownActionFactory: ActionFactory<{ url: string; openInNewTab: boolean }> = { + type: 'Url', + displayName: 'Go to URL', + iconType: 'link', + createConfig: () => { + return { + url: '', + openInNewTab: false, + }; + }, + isValid: config => { + if (!config.url) return false; + return true; + }, + wizard: props => { + const config = props.config ?? { + url: '', + openInNewTab: false, + }; + return ( + <> + <EuiFormRow label="Enter target URL"> + <EuiFieldText + placeholder="Enter URL" + name="url" + value={config.url} + onChange={event => props.onConfig({ ...config, url: event.target.value })} + /> + </EuiFormRow> + <EuiFormRow hasChildLabel={false}> + <EuiSwitch + name="openInNewTab" + label="Open in new tab?" + checked={config.openInNewTab} + onChange={() => props.onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + </EuiFormRow> + </> + ); + }, +}; + +export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory<any>> }) { + const [state, setState] = useState<{ + currentActionFactory?: ActionFactory; + config?: ActionBaseConfig; + }>({}); + + function changeActionFactory(newActionFactory: ActionFactory | null) { + if (!newActionFactory) { + // removing action factory + return setState({}); + } + + setState({ + currentActionFactory: newActionFactory, + config: newActionFactory.createConfig(), + }); + } + + return ( + <> + <ActionWizard + actionFactories={actionFactories} + config={state.config} + onConfigChange={newConfig => { + setState({ + ...state, + config: newConfig, + }); + }} + onActionFactoryChange={newActionFactory => { + changeActionFactory(newActionFactory); + }} + currentActionFactory={state.currentActionFactory} + /> + <div style={{ marginTop: '44px' }} /> + <hr /> + <div>Action Factory Type: {state.currentActionFactory?.type}</div> + <div>Action Factory Config: {JSON.stringify(state.config)}</div> + <div> + Is config valid:{' '} + {JSON.stringify(state.currentActionFactory?.isValid(state.config!) ?? false)} + </div> + </> + ); +} diff --git a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js b/x-pack/plugins/advanced_ui_actions/scripts/storybook.js new file mode 100644 index 0000000000000..3da0a3b37bfaf --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/scripts/storybook.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'advanced_ui_actions', + storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], +}); diff --git a/x-pack/plugins/alerting/server/lib/parse_duration.test.ts b/x-pack/plugins/alerting/common/parse_duration.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/parse_duration.test.ts rename to x-pack/plugins/alerting/common/parse_duration.test.ts diff --git a/x-pack/plugins/alerting/server/lib/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts similarity index 95% rename from x-pack/plugins/alerting/server/lib/parse_duration.ts rename to x-pack/plugins/alerting/common/parse_duration.ts index 51f3d746a6869..4e35a4c4cb0cf 100644 --- a/x-pack/plugins/alerting/server/lib/parse_duration.ts +++ b/x-pack/plugins/alerting/common/parse_duration.ts @@ -8,6 +8,7 @@ const MINUTES_REGEX = /^[1-9][0-9]*m$/; const HOURS_REGEX = /^[1-9][0-9]*h$/; const DAYS_REGEX = /^[1-9][0-9]*d$/; +// parse an interval string '{digit*}{s|m|h|d}' into milliseconds export function parseDuration(duration: string): number { const parsed = parseInt(duration, 10); if (isSeconds(duration)) { diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index ed6e7562e3acd..0e929ff457fbd 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -1984,6 +1984,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2101,6 +2102,7 @@ describe('update()', () => { "tags": Array [ "foo", ], + "throttle": null, "updatedBy": "elastic", } `); @@ -2186,6 +2188,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: '5m', actions: [ { group: 'default', @@ -2254,6 +2257,7 @@ describe('update()', () => { "tags": Array [ "foo", ], + "throttle": "5m", "updatedBy": "elastic", } `); @@ -2332,6 +2336,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: '5m', actions: [ { group: 'default', @@ -2401,6 +2406,7 @@ describe('update()', () => { "tags": Array [ "foo", ], + "throttle": "5m", "updatedBy": "elastic", } `); @@ -2441,6 +2447,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2509,6 +2516,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2613,6 +2621,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: '5m', actions: [ { group: 'default', @@ -2742,6 +2751,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2772,6 +2782,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2808,6 +2819,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', @@ -2843,6 +2855,7 @@ describe('update()', () => { params: { bar: true, }, + throttle: null, actions: [ { group: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 9a56781aa1d7d..49c80af0072c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -107,6 +107,7 @@ interface UpdateOptions { schedule: IntervalSchedule; actions: NormalizedAlertAction[]; params: Record<string, any>; + throttle: string | null; }; } diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index c84825cadbd16..2f610aafd8c31 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { parseDuration, validateDurationSchema } from './parse_duration'; +export { parseDuration, validateDurationSchema } from '../../common/parse_duration'; export { LicenseState } from './license_state'; export { validateAlertTypeParams } from './validate_alert_type_params'; diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index 005367ef7979c..c3628617f861f 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -116,6 +116,7 @@ describe('updateAlertRoute', () => { "tags": Array [ "bar", ], + "throttle": null, }, "id": "1", }, diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/update.ts index 76b864a51aec6..087fec2207284 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/update.ts @@ -62,11 +62,11 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => } const alertsClient = context.alerting.getAlertsClient(); const { id } = req.params; - const { name, actions, params, schedule, tags } = req.body; + const { name, actions, params, schedule, tags, throttle } = req.body; return res.ok({ body: await alertsClient.update({ id, - data: { name, actions, params, schedule, tags }, + data: { name, actions, params, schedule, tags, throttle }, }), }); }) diff --git a/x-pack/legacy/plugins/transform/public/index.ts b/x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts similarity index 51% rename from x-pack/legacy/plugins/transform/public/index.ts rename to x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts index 28c9c06f86e24..63873918b0231 100644 --- a/x-pack/legacy/plugins/transform/public/index.ts +++ b/x-pack/plugins/alerting_builtins/common/alert_types/index_threshold/index.ts @@ -3,9 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin as TransformPlugin } from './plugin'; -import { createPublicShim } from './shim'; -const { core, plugins } = createPublicShim(); -const transformPlugin = new TransformPlugin(); -transformPlugin.start(core, plugins); +export interface TimeSeriesResult { + results: TimeSeriesResultRow[]; +} + +export interface TimeSeriesResultRow { + group: string; + metrics: MetricResult[]; +} + +export type MetricResult = [string, number]; // [iso date, value] diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts index d67d29cacde42..109785b835bdf 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts @@ -64,15 +64,15 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.index = ''; expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` "[index]: types that failed validation: -- [index.0]: value is [] but it must have a minimum length of [1]. -- [index.1]: could not parse array value from []" +- [index.0]: value has length [0] but it must have a minimum length of [1]. +- [index.1]: could not parse array value from json input" `); params.index = ['', 'a']; expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` "[index]: types that failed validation: - [index.0]: expected value of type [string] but got [Array] -- [index.1.0]: value is [] but it must have a minimum length of [1]." +- [index.1.0]: value has length [0] but it must have a minimum length of [1]." `); }); @@ -89,7 +89,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.timeField = ''; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( - `"[timeField]: value is [] but it must have a minimum length of [1]."` + `"[timeField]: value has length [0] but it must have a minimum length of [1]."` ); }); @@ -113,7 +113,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.aggField = ''; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( - `"[aggField]: value is [] but it must have a minimum length of [1]."` + `"[aggField]: value has length [0] but it must have a minimum length of [1]."` ); }); @@ -126,7 +126,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.termField = ''; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( - `"[termField]: value is [] but it must have a minimum length of [1]."` + `"[termField]: value has length [0] but it must have a minimum length of [1]."` ); }); @@ -145,7 +145,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.termSize = 0; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( - `"[termSize]: Value is [0] but it must be equal to or greater than [1]."` + `"[termSize]: Value must be equal to or greater than [1]."` ); }); @@ -157,7 +157,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a params.timeWindowSize = 0; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( - `"[timeWindowSize]: Value is [0] but it must be equal to or greater than [1]."` + `"[timeWindowSize]: Value must be equal to or greater than [1]."` ); }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index 6cb21a1581113..abe5d562027eb 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -18,19 +18,11 @@ import { getDateStartAfterDateEndErrorMessage, } from './date_range_info'; -// The result is an object with a key for every field value aggregated -// via the `aggField` property. If `aggField` is not specified, the -// object will have a single key of `all documents`. The value associated -// with each key is an array of 2-tuples of `[ ISO-date, calculated-value ]` - -export interface TimeSeriesResult { - results: TimeSeriesResultRow[]; -} -export interface TimeSeriesResultRow { - group: string; - metrics: MetricResult[]; -} -export type MetricResult = [string, number]; // [iso date, value] +export { + TimeSeriesResult, + TimeSeriesResultRow, + MetricResult, +} from '../../../../common/alert_types/index_threshold'; // The parameters here are very similar to the alert parameters. // Missing are `comparator` and `threshold`, which aren't needed to generate diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 3ac47004279b3..ef1934235807b 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -53,7 +53,8 @@ describe('timeseriesFetcher', () => { 'apm_oss.spanIndices': 'apm-*', 'apm_oss.transactionIndices': 'apm-*', 'apm_oss.metricsIndices': 'apm-*', - apmAgentConfigurationIndex: '.apm-agent-configuration' + apmAgentConfigurationIndex: '.apm-agent-configuration', + apmCustomLinkIndex: '.apm-custom-link' }, dynamicIndexPattern: null as any } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_or_update_index.ts b/x-pack/plugins/apm/server/lib/helpers/create_or_update_index.ts new file mode 100644 index 0000000000000..0a0da332e73ae --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_or_update_index.ts @@ -0,0 +1,91 @@ +/* + * 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 { IClusterClient, Logger } from 'src/core/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export type Mappings = + | { + dynamic?: boolean; + properties: Record<string, Mappings>; + } + | { + type: string; + ignore_above?: number; + scaling_factor?: number; + ignore_malformed?: boolean; + coerce?: boolean; + }; + +export async function createOrUpdateIndex({ + index, + mappings, + esClient, + logger +}: { + index: string; + mappings: Mappings; + esClient: IClusterClient; + logger: Logger; +}) { + try { + const { callAsInternalUser } = esClient; + const indexExists = await callAsInternalUser('indices.exists', { index }); + const result = indexExists + ? await updateExistingIndex({ + index, + callAsInternalUser, + mappings + }) + : await createNewIndex({ + index, + callAsInternalUser, + mappings + }); + + if (!result.acknowledged) { + const resultError = + result && result.error && JSON.stringify(result.error); + throw new Error(resultError); + } + } catch (e) { + logger.error(`Could not create APM index: '${index}'. Error: ${e.message}`); + } +} + +function createNewIndex({ + index, + callAsInternalUser, + mappings +}: { + index: string; + callAsInternalUser: CallCluster; + mappings: Mappings; +}) { + return callAsInternalUser('indices.create', { + index, + body: { + // auto_expand_replicas: Allows cluster to not have replicas for this index + settings: { 'index.auto_expand_replicas': '0-1' }, + mappings + } + }); +} + +function updateExistingIndex({ + index, + callAsInternalUser, + mappings +}: { + index: string; + callAsInternalUser: CallCluster; + mappings: Mappings; +}) { + return callAsInternalUser('indices.putMapping', { + index, + body: mappings + }); +} diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 8cfb7e7edb4c6..bc03138e0c247 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -5,8 +5,11 @@ */ import { IClusterClient, Logger } from 'src/core/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { APMConfig } from '../../..'; +import { + createOrUpdateIndex, + Mappings +} from '../../helpers/create_or_update_index'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export async function createApmAgentConfigurationIndex({ @@ -18,87 +21,54 @@ export async function createApmAgentConfigurationIndex({ config: APMConfig; logger: Logger; }) { - try { - const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; - const { callAsInternalUser } = esClient; - const indexExists = await callAsInternalUser('indices.exists', { index }); - const result = indexExists - ? await updateExistingIndex(index, callAsInternalUser) - : await createNewIndex(index, callAsInternalUser); - - if (!result.acknowledged) { - const resultError = - result && result.error && JSON.stringify(result.error); - throw new Error( - `Unable to create APM Agent Configuration index '${index}': ${resultError}` - ); - } - } catch (e) { - logger.error(`Could not create APM Agent configuration: ${e.message}`); - } + const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; + return createOrUpdateIndex({ index, esClient, logger, mappings }); } -function createNewIndex(index: string, callWithInternalUser: CallCluster) { - return callWithInternalUser('indices.create', { - index, - body: { - settings: { 'index.auto_expand_replicas': '0-1' }, - mappings: { properties: mappingProperties } - } - }); -} - -// Necessary for migration reasons -// Added in 7.5: `capture_body`, `transaction_max_spans`, `applied_by_agent`, `agent_name` and `etag` -function updateExistingIndex(index: string, callWithInternalUser: CallCluster) { - return callWithInternalUser('indices.putMapping', { - index, - body: { properties: mappingProperties } - }); -} - -const mappingProperties = { - '@timestamp': { - type: 'date' - }, - service: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024 - }, - environment: { - type: 'keyword', - ignore_above: 1024 +const mappings: Mappings = { + properties: { + '@timestamp': { + type: 'date' + }, + service: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024 + }, + environment: { + type: 'keyword', + ignore_above: 1024 + } } - } - }, - settings: { - properties: { - transaction_sample_rate: { - type: 'scaled_float', - scaling_factor: 1000, - ignore_malformed: true, - coerce: false - }, - capture_body: { - type: 'keyword', - ignore_above: 1024 - }, - transaction_max_spans: { - type: 'short' + }, + settings: { + properties: { + transaction_sample_rate: { + type: 'scaled_float', + scaling_factor: 1000, + ignore_malformed: true, + coerce: false + }, + capture_body: { + type: 'keyword', + ignore_above: 1024 + }, + transaction_max_spans: { + type: 'short' + } } + }, + applied_by_agent: { + type: 'boolean' + }, + agent_name: { + type: 'keyword', + ignore_above: 1024 + }, + etag: { + type: 'keyword', + ignore_above: 1024 } - }, - applied_by_agent: { - type: 'boolean' - }, - agent_name: { - type: 'keyword', - ignore_above: 1024 - }, - etag: { - type: 'keyword', - ignore_above: 1024 } }; diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index 00493e53f06dd..f338ee058842c 100644 --- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -25,6 +25,7 @@ export interface ApmIndicesConfig { 'apm_oss.transactionIndices': string; 'apm_oss.metricsIndices': string; apmAgentConfigurationIndex: string; + apmCustomLinkIndex: string; } export type ApmIndicesName = keyof ApmIndicesConfig; @@ -52,7 +53,8 @@ export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { 'apm_oss.transactionIndices': config['apm_oss.transactionIndices'], 'apm_oss.metricsIndices': config['apm_oss.metricsIndices'], // system indices, not configurable - apmAgentConfigurationIndex: '.apm-agent-configuration' + apmAgentConfigurationIndex: '.apm-agent-configuration', + apmCustomLinkIndex: '.apm-custom-link' }; } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/__snapshots__/list_custom_links.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/__snapshots__/list_custom_links.test.ts.snap new file mode 100644 index 0000000000000..b3819ace40d6c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/__snapshots__/list_custom_links.test.ts.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`List Custom Links fetches all custom links 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [], + }, + }, + }, + "index": "myIndex", + "size": 500, +} +`; + +exports[`List Custom Links filters custom links 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, + }, + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "term": Object { + "transaction.name": "bar", + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "transaction.name", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + "index": "myIndex", + "size": 500, +} +`; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/create_or_update_custom_link.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/create_or_update_custom_link.test.ts new file mode 100644 index 0000000000000..624f01c649322 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/create_or_update_custom_link.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { createOrUpdateCustomLink } from '../create_or_update_custom_link'; +import { CustomLink } from '../custom_link_types'; +import { Setup } from '../../../helpers/setup_request'; +import { mockNow } from '../../../../../../../legacy/plugins/apm/public/utils/testHelpers'; + +describe('Create or Update Custom link', () => { + const internalClientIndexMock = jest.fn(); + const mockedSetup = ({ + internalClient: { + index: internalClientIndexMock + }, + indices: { + apmCustomLinkIndex: 'apmCustomLinkIndex' + } + } as unknown) as Setup; + + const customLink = ({ + label: 'foo', + url: 'http://elastic.com/{{trace.id}}', + 'service.name': 'opbeans-java', + 'transaction.type': 'Request' + } as unknown) as CustomLink; + afterEach(() => { + internalClientIndexMock.mockClear(); + }); + + beforeAll(() => { + mockNow(1570737000000); + }); + + it('creates a new custom link', () => { + createOrUpdateCustomLink({ customLink, setup: mockedSetup }); + expect(internalClientIndexMock).toHaveBeenCalledWith({ + refresh: true, + index: 'apmCustomLinkIndex', + body: { + '@timestamp': 1570737000000, + label: 'foo', + url: 'http://elastic.com/{{trace.id}}', + 'service.name': 'opbeans-java', + 'transaction.type': 'Request' + } + }); + }); + it('update a new custom link', () => { + createOrUpdateCustomLink({ + customLinkId: 'bar', + customLink, + setup: mockedSetup + }); + expect(internalClientIndexMock).toHaveBeenCalledWith({ + refresh: true, + index: 'apmCustomLinkIndex', + id: 'bar', + body: { + '@timestamp': 1570737000000, + label: 'foo', + url: 'http://elastic.com/{{trace.id}}', + 'service.name': 'opbeans-java', + 'transaction.type': 'Request' + } + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/list_custom_links.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/list_custom_links.test.ts new file mode 100644 index 0000000000000..5466225dc3211 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__test__/list_custom_links.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { listCustomLinks } from '../list_custom_links'; +import { + inspectSearchParams, + SearchParamsMock +} from '../../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +import { Setup } from '../../../helpers/setup_request'; +import { + SERVICE_NAME, + TRANSACTION_NAME +} from '../../../../../common/elasticsearch_fieldnames'; + +describe('List Custom Links', () => { + let mock: SearchParamsMock; + + it('fetches all custom links', async () => { + mock = await inspectSearchParams(setup => + listCustomLinks({ + setup: (setup as unknown) as Setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('filters custom links', async () => { + const filters = { + [SERVICE_NAME]: 'foo', + [TRANSACTION_NAME]: 'bar' + }; + mock = await inspectSearchParams(setup => + listCustomLinks({ + filters, + setup: (setup as unknown) as Setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts new file mode 100644 index 0000000000000..cdb3cff616030 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_custom_link_index.ts @@ -0,0 +1,60 @@ +/* + * 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 { IClusterClient, Logger } from 'src/core/server'; +import { APMConfig } from '../../..'; +import { + createOrUpdateIndex, + Mappings +} from '../../helpers/create_or_update_index'; +import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; + +export const createApmCustomLinkIndex = async ({ + esClient, + config, + logger +}: { + esClient: IClusterClient; + config: APMConfig; + logger: Logger; +}) => { + const index = getApmIndicesConfig(config).apmCustomLinkIndex; + return createOrUpdateIndex({ index, esClient, logger, mappings }); +}; + +const mappings: Mappings = { + properties: { + '@timestamp': { + type: 'date' + }, + label: { + type: 'text' + }, + url: { + type: 'keyword' + }, + service: { + properties: { + name: { + type: 'keyword' + }, + environment: { + type: 'keyword' + } + } + }, + transaction: { + properties: { + name: { + type: 'keyword' + }, + type: { + type: 'keyword' + } + } + } + } +}; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts new file mode 100644 index 0000000000000..809fe2050a072 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pick } from 'lodash'; +import { filterOptions } from '../../../routes/settings/custom_link'; +import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { Setup } from '../../helpers/setup_request'; +import { CustomLink } from './custom_link_types'; + +export async function createOrUpdateCustomLink({ + customLinkId, + customLink, + setup +}: { + customLinkId?: string; + customLink: Omit<CustomLink, '@timestamp'>; + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const params: APMIndexDocumentParams<CustomLink> = { + refresh: true, + index: indices.apmCustomLinkIndex, + body: { + '@timestamp': Date.now(), + label: customLink.label, + url: customLink.url, + ...pick(customLink, filterOptions) + } + }; + + // by specifying an id elasticsearch will delete the previous doc and insert the updated doc + if (customLinkId) { + params.id = customLinkId; + } + + return internalClient.index(params); +} diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/custom_link_types.d.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/custom_link_types.d.ts new file mode 100644 index 0000000000000..60b97712713a9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/custom_link_types.d.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { FilterOptions } from '../../../routes/settings/custom_link'; + +export type CustomLink = { + id?: string; + '@timestamp': number; + label: string; + url: string; +} & FilterOptions; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts new file mode 100644 index 0000000000000..2f3ea0940cb26 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/delete_custom_link.ts @@ -0,0 +1,25 @@ +/* + * 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 { Setup } from '../../helpers/setup_request'; + +export async function deleteCustomLink({ + customLinkId, + setup +}: { + customLinkId: string; + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const params = { + refresh: 'wait_for', + index: indices.apmCustomLinkIndex, + id: customLinkId + }; + + return internalClient.delete(params); +} diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts new file mode 100644 index 0000000000000..e6052da73b0db --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.ts @@ -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 { Setup } from '../../helpers/setup_request'; +import { CustomLink } from './custom_link_types'; +import { FilterOptions } from '../../../routes/settings/custom_link'; + +export async function listCustomLinks({ + setup, + filters = {} +}: { + setup: Setup; + filters?: FilterOptions; +}) { + const { internalClient, indices } = setup; + + const esFilters = Object.entries(filters).map(([key, value]) => { + return { + bool: { + minimum_should_match: 1, + should: [ + { term: { [key]: value } }, + { bool: { must_not: [{ exists: { field: key } }] } } + ] + } + }; + }); + + const params = { + index: indices.apmCustomLinkIndex, + size: 500, + body: { + query: { + bool: { + filter: esFilters + } + } + } + }; + const resp = await internalClient.search<CustomLink>(params); + return resp.hits.hits.map(item => ({ + id: item._id, + ...item._source + })); +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index c4a0be0f48c14..02bf60d3605bd 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -28,7 +28,8 @@ function getSetup() { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex' + apmAgentConfigurationIndex: 'myIndex', + apmCustomLinkIndex: 'myIndex' }, dynamicIndexPattern: null as any }; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 9ab31be9f7219..5e443b92aa91a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -17,7 +17,8 @@ const mockIndices = { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex' + apmAgentConfigurationIndex: 'myIndex', + apmCustomLinkIndex: 'myIndex' }; function getMockSetup(esResponse: any) { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts index cc8fabe33e63d..7a3277965ef8e 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts @@ -42,7 +42,8 @@ describe('getAnomalySeries', () => { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex' + apmAgentConfigurationIndex: 'myIndex', + apmCustomLinkIndex: 'myIndex' }, dynamicIndexPattern: null as any } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index 1970e39a2752e..a87a277eb0c0e 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -41,7 +41,8 @@ describe('timeseriesFetcher', () => { 'apm_oss.spanIndices': 'myIndex', 'apm_oss.transactionIndices': 'myIndex', 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex' + apmAgentConfigurationIndex: 'myIndex', + apmCustomLinkIndex: 'myIndex' }, dynamicIndexPattern: null as any } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 773f0d4e6fac5..db14730f802a9 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -12,6 +12,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { makeApmUsageCollector } from './lib/apm_telemetry'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; +import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; import { createApmApi } from './routes/create_apm_api'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { APMConfig, mergeConfigs, APMXPackConfig } from '.'; @@ -66,6 +67,12 @@ export class APMPlugin implements Plugin<APMPluginContract> { config: currentConfig, logger }); + // create custom action index without blocking setup lifecycle + createApmCustomLinkIndex({ + esClient: core.elasticsearch.dataClient, + config: currentConfig, + logger + }); plugins.home.tutorials.registerTutorial( tutorialProvider({ diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 21392edbb2c48..34f0536a90b4d 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -59,6 +59,12 @@ import { import { createApi } from './create_api'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; import { indicesPrivilegesRoute } from './security'; +import { + createCustomLinkRoute, + updateCustomLinkRoute, + deleteCustomLinkRoute, + listCustomLinksRoute +} from './settings/custom_link'; const createApmApi = () => { const api = createApi() @@ -126,7 +132,13 @@ const createApmApi = () => { .add(serviceMapServiceNodeRoute) // security - .add(indicesPrivilegesRoute); + .add(indicesPrivilegesRoute) + + // Custom links + .add(createCustomLinkRoute) + .add(updateCustomLinkRoute) + .add(deleteCustomLinkRoute) + .add(listCustomLinksRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts new file mode 100644 index 0000000000000..5988d7f85b186 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -0,0 +1,117 @@ +/* + * 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 * as t from 'io-ts'; +import { + SERVICE_NAME, + SERVICE_ENVIRONMENT, + TRANSACTION_NAME, + TRANSACTION_TYPE +} from '../../../common/elasticsearch_fieldnames'; +import { createRoute } from '../create_route'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link'; +import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link'; +import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; + +const FilterOptionsRt = t.partial({ + [SERVICE_NAME]: t.string, + [SERVICE_ENVIRONMENT]: t.string, + [TRANSACTION_NAME]: t.string, + [TRANSACTION_TYPE]: t.string +}); + +export type FilterOptions = t.TypeOf<typeof FilterOptionsRt>; + +export const filterOptions: Array<keyof FilterOptions> = [ + SERVICE_NAME, + SERVICE_ENVIRONMENT, + TRANSACTION_TYPE, + TRANSACTION_NAME +]; + +export const listCustomLinksRoute = createRoute(core => ({ + path: '/api/apm/settings/custom_links', + params: { + query: FilterOptionsRt + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + return await listCustomLinks({ setup, filters: params.query }); + } +})); + +const payload = t.intersection([ + t.type({ + label: t.string, + url: t.string + }), + FilterOptionsRt +]); + +export const createCustomLinkRoute = createRoute(() => ({ + method: 'POST', + path: '/api/apm/settings/custom_links', + params: { + body: payload + }, + options: { + tags: ['access:apm', 'access:apm_write'] + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const customLink = context.params.body; + const res = await createOrUpdateCustomLink({ customLink, setup }); + return res; + } +})); + +export const updateCustomLinkRoute = createRoute(() => ({ + method: 'PUT', + path: '/api/apm/settings/custom_links/{id}', + params: { + path: t.type({ + id: t.string + }), + body: payload + }, + options: { + tags: ['access:apm', 'access:apm_write'] + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { id } = context.params.path; + const customLink = context.params.body; + const res = await createOrUpdateCustomLink({ + customLinkId: id, + customLink, + setup + }); + return res; + } +})); + +export const deleteCustomLinkRoute = createRoute(() => ({ + method: 'DELETE', + path: '/api/apm/settings/custom_links/{id}', + params: { + path: t.type({ + id: t.string + }) + }, + options: { + tags: ['access:apm', 'access:apm_write'] + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { id } = context.params.path; + const res = await deleteCustomLink({ + customLinkId: id, + setup + }); + return res; + } +})); diff --git a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx index 1db57eb3d0b28..4834cc8081374 100644 --- a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx @@ -25,13 +25,13 @@ export interface OpenFlyoutAddDrilldownParams { export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_ADD_DRILLDOWN> { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; - public order = 5; + public order = 100; constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} public getDisplayName() { return i18n.translate('xpack.drilldowns.FlyoutCreateDrilldownAction.displayName', { - defaultMessage: 'Create Drilldown', + defaultMessage: 'Create drilldown', }); } @@ -40,7 +40,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY } public async isCompatible({ embeddable }: FlyoutCreateDrilldownActionContext) { - return true; + return embeddable.getInput().viewMode === 'edit'; } public async execute(context: FlyoutCreateDrilldownActionContext) { diff --git a/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx new file mode 100644 index 0000000000000..f109da94fcaca --- /dev/null +++ b/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'src/core/public'; +import { EuiNotificationBadge } from '@elastic/eui'; +import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; +import { + toMountPoint, + reactToUiComponent, +} from '../../../../../../src/plugins/kibana_react/public'; +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { FormCreateDrilldown } from '../../components/form_create_drilldown'; + +export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN'; + +export interface FlyoutEditDrilldownActionContext { + embeddable: IEmbeddable; +} + +export interface FlyoutEditDrilldownParams { + overlays: () => Promise<CoreStart['overlays']>; +} + +const displayName = i18n.translate('xpack.drilldowns.panel.openFlyoutEditDrilldown.displayName', { + defaultMessage: 'Manage drilldowns', +}); + +// mocked data +const drilldrownCount = 2; + +export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOUT_EDIT_DRILLDOWN> { + public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; + public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; + public order = 100; + + constructor(protected readonly params: FlyoutEditDrilldownParams) {} + + public getDisplayName() { + return displayName; + } + + public getIconType() { + return 'list'; + } + + private ReactComp: React.FC<{ context: FlyoutEditDrilldownActionContext }> = () => { + return ( + <> + {displayName}{' '} + <EuiNotificationBadge color="subdued" style={{ float: 'right' }}> + {drilldrownCount} + </EuiNotificationBadge> + </> + ); + }; + + MenuItem = reactToUiComponent(this.ReactComp); + + public async isCompatible({ embeddable }: FlyoutEditDrilldownActionContext) { + return embeddable.getInput().viewMode === 'edit' && drilldrownCount > 0; + } + + public async execute({ embeddable }: FlyoutEditDrilldownActionContext) { + const overlays = await this.params.overlays(); + overlays.openFlyout(toMountPoint(<FormCreateDrilldown />)); + } +} diff --git a/x-pack/plugins/drilldowns/public/actions/index.ts b/x-pack/plugins/drilldowns/public/actions/index.ts index ce235043b4ef6..4a0a34f08428a 100644 --- a/x-pack/plugins/drilldowns/public/actions/index.ts +++ b/x-pack/plugins/drilldowns/public/actions/index.ts @@ -5,3 +5,4 @@ */ export * from './flyout_create_drilldown'; +export * from './flyout_edit_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts index 1761e17d55986..b89172541b91e 100644 --- a/x-pack/plugins/drilldowns/public/plugin.ts +++ b/x-pack/plugins/drilldowns/public/plugin.ts @@ -7,7 +7,12 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { DrilldownService } from './service'; -import { FlyoutCreateDrilldownActionContext, OPEN_FLYOUT_ADD_DRILLDOWN } from './actions'; +import { + FlyoutCreateDrilldownActionContext, + FlyoutEditDrilldownActionContext, + OPEN_FLYOUT_ADD_DRILLDOWN, + OPEN_FLYOUT_EDIT_DRILLDOWN, +} from './actions'; export interface DrilldownsSetupDependencies { uiActions: UiActionsSetup; @@ -25,6 +30,7 @@ export interface DrilldownsStartContract {} declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [OPEN_FLYOUT_ADD_DRILLDOWN]: FlyoutCreateDrilldownActionContext; + [OPEN_FLYOUT_EDIT_DRILLDOWN]: FlyoutEditDrilldownActionContext; } } diff --git a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts index 715b0ce8e60e1..7209045191e94 100644 --- a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts +++ b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts @@ -6,17 +6,20 @@ import { CoreSetup } from 'src/core/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public'; -import { FlyoutCreateDrilldownAction } from '../actions'; +import { FlyoutCreateDrilldownAction, FlyoutEditDrilldownAction } from '../actions'; import { DrilldownsSetupDependencies } from '../plugin'; export class DrilldownService { bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) { - const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ - overlays: async () => (await core.getStartServices())[0].overlays, - }); + const overlays = async () => (await core.getStartServices())[0].overlays; + const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays }); uiActions.registerAction(actionFlyoutCreateDrilldown); uiActions.attachAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); + + const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ overlays }); + uiActions.registerAction(actionFlyoutEditDrilldown); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown); } /** diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index e05d8d687d05a..8f74c461a2a9b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -37,13 +37,13 @@ describe('config schema', () => { expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }) ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."` + `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` ); expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true }) ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."` + `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` ); }); }); diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index c88ce9c1413b3..b1e5ab015aa5f 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -31,9 +31,9 @@ export enum Direction { export class EndpointAppConstants { static BASE_API_URL = '/api/endpoint'; - static ALERT_INDEX_NAME = 'my-index'; static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; - static EVENT_INDEX_NAME = 'endpoint-events-*'; + static ALERT_INDEX_NAME = 'events-endpoint-1'; + static EVENT_INDEX_NAME = 'events-endpoint-*'; static DEFAULT_TOTAL_HITS = 10000; /** * Legacy events are stored in indices with endgame-* prefix diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts index 86dd4c053e8fa..4db8ee0bfbcef 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts @@ -7,7 +7,7 @@ import { ResolverEvent, LegacyEndpointEvent } from '../../../../common/types'; function isLegacyData(data: ResolverEvent): data is LegacyEndpointEvent { - return data.agent.type === 'endgame'; + return data.agent?.type === 'endgame'; } export function extractEventID(event: ResolverEvent) { diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index aaf3fc357352e..30929ba98d33b 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -132,7 +132,7 @@ describe('setupAuthentication()', () => { expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1); expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith(); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).not.toHaveBeenCalled(); @@ -155,7 +155,7 @@ describe('setupAuthentication()', () => { state: mockUser, requestHeaders: mockAuthHeaders, }); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); @@ -184,7 +184,7 @@ describe('setupAuthentication()', () => { requestHeaders: mockAuthHeaders, responseHeaders: mockAuthResponseHeaders, }); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); @@ -197,9 +197,9 @@ describe('setupAuthentication()', () => { await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit); - expect(mockResponse.redirected).toHaveBeenCalledTimes(1); - expect(mockResponse.redirected).toHaveBeenCalledWith({ - headers: { location: '/some/url' }, + expect(mockAuthToolkit.redirected).toHaveBeenCalledTimes(1); + expect(mockAuthToolkit.redirected).toHaveBeenCalledWith({ + location: '/some/url', }); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); expect(mockResponse.internalError).not.toHaveBeenCalled(); @@ -216,7 +216,7 @@ describe('setupAuthentication()', () => { expect(error).toBeUndefined(); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); expect(loggingServiceMock.collect(mockSetupAuthenticationParams.loggers).error) .toMatchInlineSnapshot(` Array [ @@ -239,7 +239,7 @@ describe('setupAuthentication()', () => { expect(response.body).toBe(esError); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); }); it('includes `WWW-Authenticate` header if `authenticate` fails to authenticate user and provides challenges', async () => { @@ -264,22 +264,19 @@ describe('setupAuthentication()', () => { expect(options!.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' }); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); }); - it('returns `unauthorized` when authentication can not be handled', async () => { + it('returns `notHandled` when authentication can not be handled', async () => { const mockResponse = httpServerMock.createLifecycleResponseFactory(); authenticate.mockResolvedValue(AuthenticationResult.notHandled()); await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit); - expect(mockResponse.unauthorized).toHaveBeenCalledTimes(1); - const [[response]] = mockResponse.unauthorized.mock.calls; - - expect(response!.body).toBeUndefined(); + expect(mockAuthToolkit.notHandled).toHaveBeenCalledTimes(1); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.redirected).not.toHaveBeenCalled(); + expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 189babbc6bfe6..1eed53efc6441 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -127,10 +127,8 @@ export async function setupAuthentication({ // authentication (username and password) or arbitrary external page managed by 3rd party // Identity Provider for SSO authentication mechanisms. Authentication provider is the one who // decides what location user should be redirected to. - return response.redirected({ - headers: { - location: authenticationResult.redirectURL!, - }, + return t.redirected({ + location: authenticationResult.redirectURL!, }); } @@ -153,9 +151,7 @@ export async function setupAuthentication({ } authLogger.debug('Could not handle authentication attempt'); - return response.unauthorized({ - headers: authenticationResult.authResponseHeaders, - }); + return t.notHandled(); }); authLogger.debug('Successfully registered core authentication handler.'); diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 00a50dd5b8821..4cbc76ecb6be4 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -112,8 +112,7 @@ export function setupAuthorization({ authz ); - // if we're an anonymous route, we disable all ui capabilities - if (request.route.options.authRequired === false) { + if (!request.auth.isAuthenticated) { return disableUICapabilities.all(capabilities); } diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 9f7f2736766ed..03285184d6572 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -103,13 +103,13 @@ describe('config schema', () => { expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }) ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."` + `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` ); expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true }) ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."` + `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` ); }); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts index 694d0fca97a2c..cd3b871671551 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -85,17 +85,17 @@ describe('Basic authentication routes', () => { expect(() => bodyValidator.validate({ username: '', password: '' }) ).toThrowErrorMatchingInlineSnapshot( - `"[username]: value is [] but it must have a minimum length of [1]."` + `"[username]: value has length [0] but it must have a minimum length of [1]."` ); expect(() => bodyValidator.validate({ username: 'user', password: '' }) ).toThrowErrorMatchingInlineSnapshot( - `"[password]: value is [] but it must have a minimum length of [1]."` + `"[password]: value has length [0] but it must have a minimum length of [1]."` ); expect(() => bodyValidator.validate({ username: '', password: 'password' }) ).toThrowErrorMatchingInlineSnapshot( - `"[username]: value is [] but it must have a minimum length of [1]."` + `"[username]: value has length [0] but it must have a minimum length of [1]."` ); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts index e9ba5c41c3988..acde73dcd8190 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.test.ts @@ -104,7 +104,7 @@ describe('Put payload schema', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "[kibana.0.spaces]: types that failed validation: -- [kibana.0.spaces.0.0]: expected value to equal [*] but got [foo-*] +- [kibana.0.spaces.0.0]: expected value to equal [*] - [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed" `); }); @@ -116,7 +116,7 @@ describe('Put payload schema', () => { }) ).toThrowErrorMatchingInlineSnapshot(` "[kibana.0.spaces]: types that failed validation: -- [kibana.0.spaces.0.1]: expected value to equal [*] but got [foo-space] +- [kibana.0.spaces.0.1]: expected value to equal [*] - [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed" `); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index d19debe692460..62b49f0c4e7f0 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -124,7 +124,7 @@ describe('PUT role', () => { expect(() => requestParamsSchema.validate({ name: '' }, {}, 'request params') ).toThrowErrorMatchingInlineSnapshot( - `"[request params.name]: value is [] but it must have a minimum length of [1]."` + `"[request params.name]: value has length [0] but it must have a minimum length of [1]."` ); }); @@ -132,7 +132,7 @@ describe('PUT role', () => { expect(() => requestParamsSchema.validate({ name: 'a'.repeat(1025) }, {}, 'request params') ).toThrowErrorMatchingInlineSnapshot( - `"[request params.name]: value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must have a maximum length of [1024]."` + `"[request params.name]: value has length [1025] but it must have a maximum length of [1024]."` ); }); }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index b40a4e406205c..c2db34dc3c33c 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -82,12 +82,12 @@ describe('Change password', () => { `"[username]: expected value of type [string] but got [undefined]"` ); expect(() => paramsSchema.validate({ username: '' })).toThrowErrorMatchingInlineSnapshot( - `"[username]: value is [] but it must have a minimum length of [1]."` + `"[username]: value has length [0] but it must have a minimum length of [1]."` ); expect(() => paramsSchema.validate({ username: 'a'.repeat(1025) }) ).toThrowErrorMatchingInlineSnapshot( - `"[username]: value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must have a maximum length of [1024]."` + `"[username]: value has length [1025] but it must have a maximum length of [1024]."` ); const bodySchema = (routeConfig.validate as any).body as ObjectType; @@ -95,12 +95,12 @@ describe('Change password', () => { `"[newPassword]: expected value of type [string] but got [undefined]"` ); expect(() => bodySchema.validate({ newPassword: '' })).toThrowErrorMatchingInlineSnapshot( - `"[newPassword]: value is [] but it must have a minimum length of [1]."` + `"[newPassword]: value has length [0] but it must have a minimum length of [1]."` ); expect(() => bodySchema.validate({ newPassword: '123456', password: '' }) ).toThrowErrorMatchingInlineSnapshot( - `"[password]: value is [] but it must have a minimum length of [1]."` + `"[password]: value has length [0] but it must have a minimum length of [1]."` ); }); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.test.ts b/x-pack/plugins/spaces/server/lib/space_schema.test.ts index 6330fcef19e8d..3a4bc080f5a9b 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.test.ts +++ b/x-pack/plugins/spaces/server/lib/space_schema.test.ts @@ -93,7 +93,7 @@ describe('#disabledFeatures', () => { disabledFeatures: 'foo', }) ).toThrowErrorMatchingInlineSnapshot( - `"[disabledFeatures]: could not parse array value from [foo]"` + `"[disabledFeatures]: could not parse array value from json input"` ); }); diff --git a/x-pack/legacy/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts similarity index 97% rename from x-pack/legacy/plugins/transform/common/constants.ts rename to x-pack/plugins/transform/common/constants.ts index 39138c12c8299..b01a82dffa04a 100644 --- a/x-pack/legacy/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants'; +import { LICENSE_TYPE_BASIC, LicenseType } from '../../../legacy/common/constants'; export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; diff --git a/x-pack/plugins/transform/common/index.ts b/x-pack/plugins/transform/common/index.ts new file mode 100644 index 0000000000000..d7a791e78b3ab --- /dev/null +++ b/x-pack/plugins/transform/common/index.ts @@ -0,0 +1,40 @@ +/* + * 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 MissingPrivileges { + [key: string]: string[] | undefined; +} + +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} + +export type TransformId = string; + +// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 +export enum TRANSFORM_STATE { + ABORTING = 'aborting', + FAILED = 'failed', + INDEXING = 'indexing', + STARTED = 'started', + STOPPED = 'stopped', + STOPPING = 'stopping', +} + +export interface TransformEndpointRequest { + id: TransformId; + state?: TRANSFORM_STATE; +} + +export interface ResultData { + success: boolean; + error?: any; +} + +export interface TransformEndpointResult { + [key: string]: ResultData; +} diff --git a/x-pack/legacy/plugins/transform/common/types/common.ts b/x-pack/plugins/transform/common/types/common.ts similarity index 100% rename from x-pack/legacy/plugins/transform/common/types/common.ts rename to x-pack/plugins/transform/common/types/common.ts diff --git a/x-pack/legacy/plugins/transform/common/types/messages.ts b/x-pack/plugins/transform/common/types/messages.ts similarity index 100% rename from x-pack/legacy/plugins/transform/common/types/messages.ts rename to x-pack/plugins/transform/common/types/messages.ts diff --git a/x-pack/legacy/plugins/transform/common/utils/date_utils.ts b/x-pack/plugins/transform/common/utils/date_utils.ts similarity index 100% rename from x-pack/legacy/plugins/transform/common/utils/date_utils.ts rename to x-pack/plugins/transform/common/utils/date_utils.ts diff --git a/x-pack/legacy/plugins/transform/common/utils/es_utils.ts b/x-pack/plugins/transform/common/utils/es_utils.ts similarity index 100% rename from x-pack/legacy/plugins/transform/common/utils/es_utils.ts rename to x-pack/plugins/transform/common/utils/es_utils.ts diff --git a/x-pack/legacy/plugins/transform/common/utils/object_utils.ts b/x-pack/plugins/transform/common/utils/object_utils.ts similarity index 100% rename from x-pack/legacy/plugins/transform/common/utils/object_utils.ts rename to x-pack/plugins/transform/common/utils/object_utils.ts diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index 87e38f83ef640..391a95853cc16 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -2,13 +2,15 @@ "id": "transform", "version": "kibana", "server": true, - "ui": false, + "ui": true, "requiredPlugins": [ + "data", "home", "licensing", "management" ], "optionalPlugins": [ + "security", "usageCollection" ], "configPath": ["xpack", "transform"] diff --git a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts similarity index 82% rename from x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts rename to x-pack/plugins/transform/public/__mocks__/shared_imports.ts index d7fca9820e614..bc8ace2932c0e 100644 --- a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -14,6 +14,6 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; +export { mlInMemoryTableBasicFactory } from '../../../../legacy/plugins/ml/public/application/components/ml_in_memory_table'; export const SORT_DIRECTION = { ASC: 'asc' }; export const KqlFilterBar = jest.fn(() => null); diff --git a/x-pack/legacy/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/app.tsx rename to x-pack/plugins/transform/public/app/app.tsx index efbaabe447efa..644aedec3eac0 100644 --- a/x-pack/legacy/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -14,7 +14,7 @@ import { SectionError } from './components'; import { CLIENT_BASE_PATH, SECTION_SLUG } from './constants'; import { getAppProviders } from './app_dependencies'; import { AuthorizationContext } from './lib/authorization'; -import { AppDependencies } from '../shim'; +import { AppDependencies } from './app_dependencies'; import { CloneTransformSection } from './sections/clone_transform'; import { CreateTransformSection } from './sections/create_transform'; diff --git a/x-pack/plugins/transform/public/app/app_dependencies.mock.ts b/x-pack/plugins/transform/public/app/app_dependencies.mock.ts new file mode 100644 index 0000000000000..4e5af1eca7bd0 --- /dev/null +++ b/x-pack/plugins/transform/public/app/app_dependencies.mock.ts @@ -0,0 +1,28 @@ +/* + * 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 { coreMock } from '../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; + +import { getAppProviders, AppDependencies } from './app_dependencies'; + +const coreSetup = coreMock.createSetup(); +const coreStart = coreMock.createStart(); +const dataStart = dataPluginMock.createStartContract(); + +const appDependencies: AppDependencies = { + chrome: coreStart.chrome, + data: dataStart, + docLinks: coreStart.docLinks, + i18n: coreStart.i18n, + notifications: coreStart.notifications, + uiSettings: coreStart.uiSettings, + savedObjects: coreStart.savedObjects, + overlays: coreStart.overlays, + http: coreSetup.http, +}; + +export const Providers = getAppProviders(appDependencies); diff --git a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx similarity index 71% rename from x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx rename to x-pack/plugins/transform/public/app/app_dependencies.tsx index 21ffbf5911a21..37258dc777d87 100644 --- a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -7,27 +7,40 @@ import React, { createContext, useContext, ReactNode } from 'react'; import { HashRouter } from 'react-router-dom'; +import { CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; + import { API_BASE_PATH } from '../../common/constants'; import { setDependencyCache } from '../shared_imports'; -import { AppDependencies } from '../shim'; import { AuthorizationProvider } from './lib/authorization'; +export interface AppDependencies { + chrome: CoreStart['chrome']; + data: DataPublicPluginStart; + docLinks: CoreStart['docLinks']; + http: CoreSetup['http']; + i18n: CoreStart['i18n']; + notifications: CoreStart['notifications']; + uiSettings: CoreStart['uiSettings']; + savedObjects: CoreStart['savedObjects']; + overlays: CoreStart['overlays']; +} + let DependenciesContext: React.Context<AppDependencies>; const setAppDependencies = (deps: AppDependencies) => { const legacyBasePath = { - prepend: deps.core.http.basePath.prepend, - get: deps.core.http.basePath.get, + prepend: deps.http.basePath.prepend, + get: deps.http.basePath.get, remove: () => {}, }; setDependencyCache({ - autocomplete: deps.plugins.data.autocomplete, - docLinks: deps.core.docLinks, + autocomplete: deps.data.autocomplete, + docLinks: deps.docLinks, basePath: legacyBasePath as any, - XSRF: deps.plugins.xsrfToken, }); DependenciesContext = createContext<AppDependencies>(deps); return DependenciesContext.Provider; @@ -41,24 +54,15 @@ export const useAppDependencies = () => { return useContext<AppDependencies>(DependenciesContext); }; -export const useDocumentationLinks = () => { - const { - core: { documentation }, - } = useAppDependencies(); - return documentation; -}; - export const useToastNotifications = () => { const { - core: { - notifications: { toasts: toastNotifications }, - }, + notifications: { toasts: toastNotifications }, } = useAppDependencies(); return toastNotifications; }; export const getAppProviders = (deps: AppDependencies) => { - const I18nContext = deps.core.i18n.Context; + const I18nContext = deps.i18n.Context; // Create App dependencies context and get its provider const AppDependenciesProvider = setAppDependencies(deps); diff --git a/x-pack/legacy/plugins/transform/public/app/common/__mocks__/transform_list_row.json b/x-pack/plugins/transform/public/app/common/__mocks__/transform_list_row.json similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/__mocks__/transform_list_row.json rename to x-pack/plugins/transform/public/app/common/__mocks__/transform_list_row.json diff --git a/x-pack/legacy/plugins/transform/public/app/common/__mocks__/transform_stats.json b/x-pack/plugins/transform/public/app/common/__mocks__/transform_stats.json similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/__mocks__/transform_stats.json rename to x-pack/plugins/transform/public/app/common/__mocks__/transform_stats.json diff --git a/x-pack/legacy/plugins/transform/public/app/common/aggregations.test.ts b/x-pack/plugins/transform/public/app/common/aggregations.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/aggregations.test.ts rename to x-pack/plugins/transform/public/app/common/aggregations.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts similarity index 83% rename from x-pack/legacy/plugins/transform/public/app/common/aggregations.ts rename to x-pack/plugins/transform/public/app/common/aggregations.ts index 038d68ff37d87..f098e933e4b13 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { composeValidators, patternValidator } from '../../../../ml/common/util/validators'; +import { + composeValidators, + patternValidator, +} from '../../../../../legacy/plugins/ml/common/util/validators'; export type AggName = string; diff --git a/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/data_grid.ts rename to x-pack/plugins/transform/public/app/common/data_grid.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/dropdown.ts b/x-pack/plugins/transform/public/app/common/dropdown.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/dropdown.ts rename to x-pack/plugins/transform/public/app/common/dropdown.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/fields.ts b/x-pack/plugins/transform/public/app/common/fields.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/fields.ts rename to x-pack/plugins/transform/public/app/common/fields.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/common/index.ts rename to x-pack/plugins/transform/public/app/common/index.ts index 52a6884367bc5..e81fadddbea69 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -22,7 +22,6 @@ export { useRefreshTransformList, CreateRequestBody, PreviewRequestBody, - TransformId, TransformPivotConfig, IndexName, IndexPattern, @@ -35,7 +34,6 @@ export { isTransformStats, TransformStats, TRANSFORM_MODE, - TRANSFORM_STATE, } from './transform_stats'; export { getDiscoverUrl } from './navigation'; export { diff --git a/x-pack/legacy/plugins/transform/public/app/common/navigation.test.tsx b/x-pack/plugins/transform/public/app/common/navigation.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/navigation.test.tsx rename to x-pack/plugins/transform/public/app/common/navigation.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx b/x-pack/plugins/transform/public/app/common/navigation.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/navigation.tsx rename to x-pack/plugins/transform/public/app/common/navigation.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts rename to x-pack/plugins/transform/public/app/common/pivot_aggs.ts index af55732691bb0..3ea614aaf5c9a 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; +import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts rename to x-pack/plugins/transform/public/app/common/pivot_group_by.ts index e6792958ab5d2..bd5a5a26d9019 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; +import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/request.test.ts rename to x-pack/plugins/transform/public/app/common/request.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/common/request.ts rename to x-pack/plugins/transform/public/app/common/request.ts index 31089b86a2c2d..79fb3acb9fcaf 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -12,7 +12,7 @@ import { SavedSearchQuery } from '../hooks/use_search_items'; import { StepDefineExposedState } from '../sections/create_transform/components/step_define/step_define_form'; import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; import { getEsAggFromAggConfig, diff --git a/x-pack/legacy/plugins/transform/public/app/common/transform.test.ts b/x-pack/plugins/transform/public/app/common/transform.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/transform.test.ts rename to x-pack/plugins/transform/public/app/common/transform.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/common/transform.ts rename to x-pack/plugins/transform/public/app/common/transform.ts index 481ad3c6d74ff..7cf7412283201 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/transform.ts +++ b/x-pack/plugins/transform/public/app/common/transform.ts @@ -9,12 +9,13 @@ import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; +import { TransformId } from '../../../common'; + import { PivotAggDict } from './pivot_aggs'; import { PivotGroupByDict } from './pivot_group_by'; export type IndexName = string; export type IndexPattern = string; -export type TransformId = string; // Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; // It must also start and end with an alphanumeric character. diff --git a/x-pack/legacy/plugins/transform/public/app/common/transform_list.ts b/x-pack/plugins/transform/public/app/common/transform_list.ts similarity index 87% rename from x-pack/legacy/plugins/transform/public/app/common/transform_list.ts rename to x-pack/plugins/transform/public/app/common/transform_list.ts index 8925923ed9d8f..17d729a453a05 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/transform_list.ts +++ b/x-pack/plugins/transform/public/app/common/transform_list.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId, TransformPivotConfig } from './transform'; +import { TransformId } from '../../../common'; +import { TransformPivotConfig } from './transform'; import { TransformStats } from './transform_stats'; // Used to pass on attribute names to table columns diff --git a/x-pack/legacy/plugins/transform/public/app/common/transform_stats.test.ts b/x-pack/plugins/transform/public/app/common/transform_stats.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/transform_stats.test.ts rename to x-pack/plugins/transform/public/app/common/transform_stats.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/transform_stats.ts b/x-pack/plugins/transform/public/app/common/transform_stats.ts similarity index 84% rename from x-pack/legacy/plugins/transform/public/app/common/transform_stats.ts rename to x-pack/plugins/transform/public/app/common/transform_stats.ts index 433616e422802..72df6d3985e23 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/transform_stats.ts +++ b/x-pack/plugins/transform/public/app/common/transform_stats.ts @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId } from './transform'; -import { TransformListRow } from './transform_list'; +import { TransformId, TRANSFORM_STATE } from '../../../common'; -// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 -export enum TRANSFORM_STATE { - ABORTING = 'aborting', - FAILED = 'failed', - INDEXING = 'indexing', - STARTED = 'started', - STOPPED = 'stopped', - STOPPING = 'stopping', -} +import { TransformListRow } from './transform_list'; export enum TRANSFORM_MODE { BATCH = 'batch', diff --git a/x-pack/legacy/plugins/transform/public/app/common/validators.test.ts b/x-pack/plugins/transform/public/app/common/validators.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/validators.test.ts rename to x-pack/plugins/transform/public/app/common/validators.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/validators.ts b/x-pack/plugins/transform/public/app/common/validators.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/common/validators.ts rename to x-pack/plugins/transform/public/app/common/validators.ts diff --git a/x-pack/legacy/plugins/transform/public/app/components/index.ts b/x-pack/plugins/transform/public/app/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/components/index.ts rename to x-pack/plugins/transform/public/app/components/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/components/job_icon.tsx b/x-pack/plugins/transform/public/app/components/job_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/components/job_icon.tsx rename to x-pack/plugins/transform/public/app/components/job_icon.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx b/x-pack/plugins/transform/public/app/components/section_error.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/components/section_error.tsx rename to x-pack/plugins/transform/public/app/components/section_error.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/components/section_loading.tsx b/x-pack/plugins/transform/public/app/components/section_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/components/section_loading.tsx rename to x-pack/plugins/transform/public/app/components/section_loading.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx similarity index 86% rename from x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx rename to x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx index 095b57de97d9a..5b8721cb0fe8c 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { createPublicShim } from '../../shim'; -import { getAppProviders } from '../app_dependencies'; +import { Providers } from '../app_dependencies.mock'; import { ToastNotificationText } from './toast_notification_text'; @@ -17,7 +16,6 @@ jest.mock('ui/new_platform'); describe('ToastNotificationText', () => { test('should render the text as plain text', () => { - const Providers = getAppProviders(createPublicShim()); const props = { text: 'a short text message', }; @@ -30,7 +28,6 @@ describe('ToastNotificationText', () => { }); test('should render the text within a modal', () => { - const Providers = getAppProviders(createPublicShim()); const props = { text: 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 characters. ', diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx similarity index 94% rename from x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx rename to x-pack/plugins/transform/public/app/components/toast_notification_text.tsx index 4e0a0a12558d8..44927e61a42c4 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx @@ -18,16 +18,14 @@ import { import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { useAppDependencies } from '../app_dependencies'; const MAX_SIMPLE_MESSAGE_LENGTH = 140; export const ToastNotificationText: FC<{ text: any }> = ({ text }) => { - const { - core: { overlays }, - } = useAppDependencies(); + const { overlays } = useAppDependencies(); if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { return text; diff --git a/x-pack/legacy/plugins/transform/public/app/constants/index.ts b/x-pack/plugins/transform/public/app/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/constants/index.ts rename to x-pack/plugins/transform/public/app/constants/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts similarity index 93% rename from x-pack/legacy/plugins/transform/public/app/hooks/__mocks__/use_api.ts rename to x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index d3f8057492201..a5cccd58211c5 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PreviewRequestBody, TransformId } from '../../common'; +import { TransformId, TransformEndpointRequest } from '../../../../common'; -import { TransformEndpointRequest } from '../use_api_types'; +import { PreviewRequestBody } from '../../common'; const apiFactory = () => ({ getTransforms(transformId?: TransformId): Promise<any> { diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/index.ts b/x-pack/plugins/transform/public/app/hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/hooks/index.ts rename to x-pack/plugins/transform/public/app/hooks/index.ts diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts new file mode 100644 index 0000000000000..c503051ed90af --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -0,0 +1,63 @@ +/* + * 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 { TransformId, TransformEndpointRequest, TransformEndpointResult } from '../../../common'; +import { API_BASE_PATH } from '../../../common/constants'; + +import { useAppDependencies } from '../app_dependencies'; +import { PreviewRequestBody } from '../common'; + +import { EsIndex } from './use_api_types'; + +export const useApi = () => { + const { http } = useAppDependencies(); + + return { + getTransforms(transformId?: TransformId): Promise<any> { + const transformIdString = transformId !== undefined ? `/${transformId}` : ''; + return http.get(`${API_BASE_PATH}transforms${transformIdString}`); + }, + getTransformsStats(transformId?: TransformId): Promise<any> { + if (transformId !== undefined) { + return http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`); + } + + return http.get(`${API_BASE_PATH}transforms/_stats`); + }, + createTransform(transformId: TransformId, transformConfig: any): Promise<any> { + return http.put(`${API_BASE_PATH}transforms/${transformId}`, { + body: JSON.stringify(transformConfig), + }); + }, + deleteTransforms(transformsInfo: TransformEndpointRequest[]): Promise<TransformEndpointResult> { + return http.post(`${API_BASE_PATH}delete_transforms`, { + body: JSON.stringify(transformsInfo), + }); + }, + getTransformsPreview(obj: PreviewRequestBody): Promise<any> { + return http.post(`${API_BASE_PATH}transforms/_preview`, { body: JSON.stringify(obj) }); + }, + startTransforms(transformsInfo: TransformEndpointRequest[]): Promise<TransformEndpointResult> { + return http.post(`${API_BASE_PATH}start_transforms`, { + body: JSON.stringify(transformsInfo), + }); + }, + stopTransforms(transformsInfo: TransformEndpointRequest[]): Promise<TransformEndpointResult> { + return http.post(`${API_BASE_PATH}stop_transforms`, { + body: JSON.stringify(transformsInfo), + }); + }, + getTransformAuditMessages(transformId: TransformId): Promise<any> { + return http.get(`${API_BASE_PATH}transforms/${transformId}/messages`); + }, + esSearch(payload: any): Promise<any> { + return http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); + }, + getIndices(): Promise<EsIndex[]> { + return http.get(`/api/index_management/indices`); + }, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts b/x-pack/plugins/transform/public/app/hooks/use_api_types.ts similarity index 84% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts rename to x-pack/plugins/transform/public/app/hooks/use_api_types.ts index d62655518f44a..1ab320692bd5e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/index.d.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api_types.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const TimeBuckets: any; +export interface EsIndex { + name: string; +} diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx rename to x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index 83f456231cb85..6210c72ef9d05 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -7,14 +7,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; -import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useDeleteTransforms = () => { const toastNotifications = useToastNotifications(); diff --git a/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts b/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts new file mode 100644 index 0000000000000..7589ee0a3e935 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAppDependencies } from '../app_dependencies'; + +import { TRANSFORM_DOC_PATHS } from '../constants'; + +export const useDocumentationLinks = () => { + const deps = useAppDependencies(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = deps.docLinks; + return { + esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`, + esIndicesCreateIndex: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/indices-create-index.html#indices-create-index`, + esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, + esQueryDsl: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl.html`, + esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`, + esTransform: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/${TRANSFORM_DOC_PATHS.transforms}`, + esTransformPivot: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/put-transform.html#put-transform-request-body`, + mlDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, + }; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_get_transforms.ts rename to x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts b/x-pack/plugins/transform/public/app/hooks/use_request.ts similarity index 89% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts rename to x-pack/plugins/transform/public/app/hooks/use_request.ts index 8c489048a77ef..e1bdbce941eb0 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_request.ts @@ -9,8 +9,6 @@ import { UseRequestConfig, useRequest as _useRequest } from '../../shared_import import { useAppDependencies } from '../app_dependencies'; export const useRequest = (config: UseRequestConfig) => { - const { - core: { http }, - } = useAppDependencies(); + const { http } = useAppDependencies(); return _useRequest(http, config); }; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/common.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/common.ts rename to x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts index 2258f8f33f01d..c536e70fd292f 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/common.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts @@ -10,7 +10,7 @@ import { esQuery, IndexPatternsContract, IndexPatternAttributes, -} from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; import { matchAllQuery } from '../../common'; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/index.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/index.ts rename to x-pack/plugins/transform/public/app/hooks/use_search_items/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts similarity index 89% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts rename to x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index 12fc75c20ffa4..f5f9e98fe659c 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -23,14 +23,14 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const [savedObjectId, setSavedObjectId] = useState(defaultSavedObjectId); const appDeps = useAppDependencies(); - const indexPatterns = appDeps.plugins.data.indexPatterns; - const uiSettings = appDeps.core.uiSettings; - const savedObjectsClient = appDeps.core.savedObjects.client; + const indexPatterns = appDeps.data.indexPatterns; + const uiSettings = appDeps.uiSettings; + const savedObjectsClient = appDeps.savedObjects.client; const savedSearches = createSavedSearchesLoader({ savedObjectsClient, indexPatterns, - chrome: appDeps.core.chrome, - overlays: appDeps.core.overlays, + chrome: appDeps.chrome, + overlays: appDeps.overlays, }); const [searchItems, setSearchItems] = useState<SearchItems | undefined>(undefined); diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_start_transform.ts index f460d8200c6e4..8e966918e4502 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; +import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; + import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; -import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStartTransforms = () => { const toastNotifications = useToastNotifications(); diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts index 758c574a3f7cd..03bc9b1ea3998 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; +import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; + import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; -import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStopTransforms = () => { const toastNotifications = useToastNotifications(); diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_x_json_mode.ts b/x-pack/plugins/transform/public/app/hooks/use_x_json_mode.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/hooks/use_x_json_mode.ts rename to x-pack/plugins/transform/public/app/hooks/use_x_json_mode.ts diff --git a/x-pack/legacy/plugins/transform/public/app/index.scss b/x-pack/plugins/transform/public/app/index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/index.scss rename to x-pack/plugins/transform/public/app/index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx rename to x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index dde63710f56aa..6553d4474d392 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -5,8 +5,12 @@ */ import React, { createContext } from 'react'; + +import { Privileges } from '../../../../../common'; + import { useRequest } from '../../../hooks'; -import { hasPrivilegeFactory, Capabilities, Privileges } from './common'; + +import { hasPrivilegeFactory, Capabilities } from './common'; interface Authorization { isLoading: boolean; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts similarity index 94% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts rename to x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index 27556e0d673a8..282a737d0bf1e 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { Privileges } from '../../../../../common'; + export interface Capabilities { canGetTransform: boolean; canDeleteTransform: boolean; @@ -16,11 +18,6 @@ export interface Capabilities { export type Privilege = [string, string]; -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} - function isPrivileges(arg: any): arg is Privileges { return ( typeof arg === 'object' && @@ -33,9 +30,6 @@ function isPrivileges(arg: any): arg is Privileges { ); } -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} export const toArray = (value: string | string[]): string[] => Array.isArray(value) ? value : [value]; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/index.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/index.ts similarity index 86% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/components/index.ts rename to x-pack/plugins/transform/public/app/lib/authorization/components/index.ts index 9b37fa1b4393d..29390ab34ea79 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/index.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createCapabilityFailureMessage, Privileges } from './common'; +export { createCapabilityFailureMessage } from './common'; export { AuthorizationProvider, AuthorizationContext } from './authorization_provider'; export { PrivilegesWrapper } from './with_privileges'; export { NotAuthorizedSection } from './not_authorized_section'; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx rename to x-pack/plugins/transform/public/app/lib/authorization/components/not_authorized_section.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx similarity index 96% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx rename to x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx index 91e5be5331203..1469e2a471cc6 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx @@ -10,11 +10,13 @@ import { EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MissingPrivileges } from '../../../../../common'; + import { SectionLoading } from '../../../components'; import { AuthorizationContext } from './authorization_provider'; import { NotAuthorizedSection } from './not_authorized_section'; -import { hasPrivilegeFactory, toArray, MissingPrivileges, Privilege } from './common'; +import { hasPrivilegeFactory, toArray, Privilege } from './common'; interface Props { /** diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/index.ts b/x-pack/plugins/transform/public/app/lib/authorization/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/lib/authorization/index.ts rename to x-pack/plugins/transform/public/app/lib/authorization/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx rename to x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 4618e96cbfd6e..373faee3b46ed 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -22,11 +22,12 @@ import { } from '@elastic/eui'; import { useApi } from '../../hooks/use_api'; +import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; -import { useAppDependencies, useDocumentationLinks } from '../../app_dependencies'; +import { useAppDependencies } from '../../app_dependencies'; import { TransformPivotConfig } from '../../common'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; @@ -65,8 +66,8 @@ export const CloneTransformSection: FC<Props> = ({ match }) => { const api = useApi(); const appDeps = useAppDependencies(); - const savedObjectsClient = appDeps.core.savedObjects.client; - const indexPatterns = appDeps.plugins.data.indexPatterns; + const savedObjectsClient = appDeps.savedObjects.client; + const indexPatterns = appDeps.data.indexPatterns; const { esTransform } = useDocumentationLinks(); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts b/x-pack/plugins/transform/public/app/sections/clone_transform/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts rename to x-pack/plugins/transform/public/app/sections/clone_transform/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/list_summary.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/popover_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_aggregation_label_form.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_aggregation_label_form.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_aggregation_label_form.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_aggregation_label_form.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_index.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_index.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/group_by_label_summary.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/list_summary.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/__snapshots__/popover_form.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/_group_by_label_form.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/_group_by_label_form.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/_group_by_label_form.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/_group_by_label_form.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/_index.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/_index.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx similarity index 80% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx index 48eff132cd753..7a1532705916f 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx @@ -5,11 +5,10 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { getPivotQuery } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; @@ -19,7 +18,8 @@ jest.mock('ui/new_platform'); jest.mock('../../../../../shared_imports'); describe('Transform: <SourceIndexPreview />', () => { - test('Minimal initialization', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { // Arrange const props = { indexPattern: { @@ -28,7 +28,6 @@ describe('Transform: <SourceIndexPreview />', () => { } as SearchItems['indexPattern'], query: getPivotQuery('the-query'), }; - const Providers = getAppProviders(createPublicShim()); const { getByText } = render( <Providers> <SourceIndexPreview {...props} /> @@ -38,5 +37,7 @@ describe('Transform: <SourceIndexPreview />', () => { // Act // Assert expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); + await wait(); + done(); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx new file mode 100644 index 0000000000000..9992f153f3b86 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import { SimpleQuery } from '../../../../common'; +import { + SOURCE_INDEX_STATUS, + useSourceIndexData, + UseSourceIndexDataReturnType, +} from './use_source_index_data'; + +jest.mock('../../../../hooks/use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('useSourceIndexData', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query, { + pageIndex: 0, + pageSize: 10, + }) + ); + const sourceIndexObj: UseSourceIndexDataReturnType = result.current; + + await waitForNextUpdate(); + + expect(sourceIndexObj.errorMessage).toBe(''); + expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); + expect(sourceIndexObj.tableItems).toEqual([]); + done(); + }); + + // TODO add more tests to check data retrieved via `api.esSearch()`. + // This needs more investigation in regards to jest's React Hooks support. +}); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx similarity index 85% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index 7a22af492e36e..6223dfc5623b7 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { StepCreateForm } from './step_create_form'; @@ -27,7 +26,6 @@ describe('Transform: <StepCreateForm />', () => { onChange() {}, }; - const Providers = getAppProviders(createPublicShim()); const { getByText } = render( <Providers> <StepCreateForm {...props} /> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 4198c2ea0260d..49be2e67ce552 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -28,7 +28,7 @@ import { EuiText, } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; @@ -75,8 +75,8 @@ export const StepCreateForm: FC<Props> = React.memo( ); const deps = useAppDependencies(); - const indexPatterns = deps.plugins.data.indexPatterns; - const uiSettings = deps.core.uiSettings; + const indexPatterns = deps.data.indexPatterns; + const uiSettings = deps.uiSettings; const toastNotifications = useToastNotifications(); useEffect(() => { @@ -464,7 +464,7 @@ export const StepCreateForm: FC<Props> = React.memo( defaultMessage: 'Use Discover to explore the transform.', } )} - href={getDiscoverUrl(indexPatternId, deps.core.http.basePath.get())} + href={getDiscoverUrl(indexPatternId, deps.http.basePath.get())} data-test-subj="transformWizardCardDiscover" /> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts index 88e009c63339a..c9a52304578ee 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts @@ -20,7 +20,7 @@ import { getPivotPreviewDevConsoleStatement, getPivotDropdownOptions, } from './common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; describe('Transform: Define Pivot Common', () => { test('customSortFactory()', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts index 35e1ea02a5cef..0779cb1339af6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts @@ -5,10 +5,7 @@ */ import { get } from 'lodash'; import { EuiComboBoxOptionOption, EuiDataGridSorting } from '@elastic/eui'; -import { - IndexPattern, - KBN_FIELD_TYPES, -} from '../../../../../../../../../../src/plugins/data/public'; +import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; import { getNestedProperty } from '../../../../../../common/utils/object_utils'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx similarity index 86% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx index 464b6e1fd9fe3..f39885f520995 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx @@ -5,11 +5,10 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { getPivotQuery, PivotAggsConfig, @@ -25,7 +24,8 @@ jest.mock('ui/new_platform'); jest.mock('../../../../../shared_imports'); describe('Transform: <PivotPreview />', () => { - test('Minimal initialization', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { // Arrange const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, @@ -49,7 +49,6 @@ describe('Transform: <PivotPreview />', () => { query: getPivotQuery('the-query'), }; - const Providers = getAppProviders(createPublicShim()); const { getByText } = render( <Providers> <PivotPreview {...props} /> @@ -59,5 +58,7 @@ describe('Transform: <PivotPreview />', () => { // Act // Assert expect(getByText('Transform pivot preview')).toBeInTheDocument(); + await wait(); + done(); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index f45ef7cfddbf9..d5cffad166831 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -5,11 +5,10 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { PivotAggsConfigDict, PivotGroupByConfigDict, @@ -24,7 +23,8 @@ jest.mock('ui/new_platform'); jest.mock('../../../../../shared_imports'); describe('Transform: <DefinePivotForm />', () => { - test('Minimal initialization', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { // Arrange const searchItems = { indexPattern: { @@ -32,7 +32,6 @@ describe('Transform: <DefinePivotForm />', () => { fields: [] as any[], } as SearchItems['indexPattern'], }; - const Providers = getAppProviders(createPublicShim()); const { getByLabelText } = render( <Providers> <StepDefineForm onChange={jest.fn()} searchItems={searchItems as SearchItems} /> @@ -42,6 +41,8 @@ describe('Transform: <DefinePivotForm />', () => { // Act // Assert expect(getByLabelText('Index pattern')).toBeInTheDocument(); + await wait(); + done(); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx similarity index 99% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index f61f54c38680e..254d867165ae6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -26,9 +26,10 @@ import { EuiSwitch, } from '@elastic/eui'; +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; -import { useDocumentationLinks, useToastNotifications } from '../../../../app_dependencies'; +import { useToastNotifications } from '../../../../app_dependencies'; import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { DropDown } from '../aggregation_dropdown'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx similarity index 88% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 0f7da50bbbade..36a662e0cb7e6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -5,11 +5,10 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { PivotAggsConfig, PivotGroupByConfig, @@ -25,7 +24,8 @@ jest.mock('ui/new_platform'); jest.mock('../../../../../shared_imports'); describe('Transform: <DefinePivotSummary />', () => { - test('Minimal initialization', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { // Arrange const searchItems = { indexPattern: { @@ -56,7 +56,6 @@ describe('Transform: <DefinePivotSummary />', () => { valid: true, }; - const Providers = getAppProviders(createPublicShim()); const { getByText } = render( <Providers> <StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} /> @@ -67,5 +66,7 @@ describe('Transform: <DefinePivotSummary />', () => { // Assert expect(getByText('Group by')).toBeInTheDocument(); expect(getByText('Aggregations')).toBeInTheDocument(); + await wait(); + done(); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/switch_modal.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx similarity index 96% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx index 1ad8ed099b241..3e972e9f92e72 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.test.tsx @@ -14,7 +14,7 @@ import { UsePivotPreviewDataReturnType, } from './use_pivot_preview_data'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; jest.mock('../../../../hooks/use_api'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts similarity index 96% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts index 84fafcad8151e..215435027d5b8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts @@ -10,10 +10,7 @@ import { dictionaryToArray } from '../../../../../../common/types/common'; import { useApi } from '../../../../hooks/use_api'; import { Dictionary } from '../../../../../../common/types/common'; -import { - IndexPattern, - ES_FIELD_TYPES, -} from '../../../../../../../../../../src/plugins/data/public'; +import { IndexPattern, ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; import { getPreviewRequestBody, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index ea9483af49302..e56a519f80803 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -10,19 +10,17 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TransformId } from '../../../../../../common'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; -import { - useAppDependencies, - useDocumentationLinks, - useToastNotifications, -} from '../../../../app_dependencies'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SearchItems } from '../../../../hooks/use_search_items'; import { useApi } from '../../../../hooks/use_api'; -import { isTransformIdValid, TransformId, TransformPivotConfig } from '../../../../common'; +import { isTransformIdValid, TransformPivotConfig } from '../../../../common'; import { EsIndexName, IndexPatternTitle } from './common'; import { delayValidator } from '../../../../common/validators'; @@ -132,7 +130,7 @@ export const StepDetailsForm: FC<Props> = React.memo( } try { - setIndexPatternTitles(await deps.plugins.data.indexPatterns.getTitles()); + setIndexPatternTitles(await deps.data.indexPatterns.getTitles()); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_index.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_index.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx rename to x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index d09fc0913590e..eaf1e09df4754 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -21,7 +21,7 @@ import { import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; -import { useDocumentationLinks } from '../../app_dependencies'; +import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/create_transform/index.ts rename to x-pack/plugins/transform/public/app/sections/create_transform/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/__snapshots__/transform_management_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/__snapshots__/create_transform_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_index.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_index.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_transform_search_dialog.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_transform_search_dialog.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_transform_search_dialog.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/_transform_search_dialog.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/refresh_transform_list_button/refresh_transform_list_button.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx index ff8bb7e2f432d..7d6b178e4bfe4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx @@ -8,7 +8,7 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui' import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectFinderUi } from '../../../../../../../../../src/plugins/saved_objects/public'; import { useAppDependencies } from '../../../../app_dependencies'; interface SearchSelectionProps { @@ -18,9 +18,8 @@ interface SearchSelectionProps { const fixedPageSize: number = 8; export const SearchSelection: FC<SearchSelectionProps> = ({ onSearchSelected }) => { - const { - core: { uiSettings, savedObjects }, - } = useAppDependencies(); + const { uiSettings, savedObjects } = useAppDependencies(); + return ( <> <EuiModalHeader> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_index.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_index.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stat.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stat.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stat.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stat.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stats_bar.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stats_bar.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stats_bar.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/_stats_bar.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stat.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/stats_bar/stats_bar.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx similarity index 85% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx index 82b9f0a292bb9..17cca6afae483 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx @@ -7,8 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { TransformListRow } from '../../../../common'; import { DeleteAction } from './action_delete'; @@ -20,8 +19,6 @@ jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions <DeleteAction />', () => { test('Minimal initialization', () => { - const Providers = getAppProviders(createPublicShim()); - const item: TransformListRow = transformListRow; const props = { disabled: false, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx index 08db7a608be2c..c20feba29f582 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx @@ -14,14 +14,14 @@ import { EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; -import { useDeleteTransforms } from '../../../../hooks'; +import { TRANSFORM_STATE } from '../../../../../../common'; +import { useDeleteTransforms } from '../../../../hooks'; import { createCapabilityFailureMessage, AuthorizationContext, } from '../../../../lib/authorization'; - -import { TransformListRow, TRANSFORM_STATE } from '../../../../common'; +import { TransformListRow } from '../../../../common'; interface DeleteActionProps { items: TransformListRow[]; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx similarity index 85% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx index 002b4ea19f967..bbdfdbbc3c121 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx @@ -7,8 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { TransformListRow } from '../../../../common'; import { StartAction } from './action_start'; @@ -20,8 +19,6 @@ jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions <StartAction />', () => { test('Minimal initialization', () => { - const Providers = getAppProviders(createPublicShim()); - const item: TransformListRow = transformListRow; const props = { disabled: false, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx similarity index 97% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx index c81661f218443..b6b8a75b4b735 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx @@ -14,14 +14,14 @@ import { EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; -import { useStartTransforms } from '../../../../hooks'; +import { TRANSFORM_STATE } from '../../../../../../common'; +import { useStartTransforms } from '../../../../hooks'; import { createCapabilityFailureMessage, AuthorizationContext, } from '../../../../lib/authorization'; - -import { TransformListRow, isCompletedBatchTransform, TRANSFORM_STATE } from '../../../../common'; +import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; interface StartActionProps { items: TransformListRow[]; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx similarity index 85% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx index e2a22765dfb98..840fbc4b9034b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx @@ -7,8 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createPublicShim } from '../../../../../shim'; -import { getAppProviders } from '../../../../app_dependencies'; +import { Providers } from '../../../../app_dependencies.mock'; import { TransformListRow } from '../../../../common'; import { StopAction } from './action_stop'; @@ -20,8 +19,6 @@ jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions <StopAction />', () => { test('Minimal initialization', () => { - const Providers = getAppProviders(createPublicShim()); - const item: TransformListRow = transformListRow; const props = { disabled: false, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx index 98aec0acf0bf6..f6b63191b538d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx @@ -8,7 +8,9 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; -import { TransformListRow, TRANSFORM_STATE } from '../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; import { createCapabilityFailureMessage, AuthorizationContext, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx similarity index 90% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx index 3e3829973e328..6a55b419e74a9 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx @@ -5,7 +5,11 @@ */ import React from 'react'; -import { TransformListRow, TRANSFORM_STATE } from '../../../../common'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; + import { CloneAction } from './action_clone'; import { StartAction } from './action_start'; import { StopAction } from './action_stop'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx similarity index 99% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index 53627d1cf2f6b..fb24ff2a12e02 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -19,6 +19,8 @@ import { RIGHT_ALIGNMENT, } from '@elastic/eui'; +import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; + import { ActionsColumnType, ComputedColumnType, @@ -28,11 +30,9 @@ import { import { getTransformProgress, - TransformId, TransformListRow, TransformStats, TRANSFORM_LIST_COLUMN, - TRANSFORM_STATE, } from '../../../../common'; import { getActions } from './actions'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts similarity index 88% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts index c2030e814aa95..11e4dc3dfa2b8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TRANSFORM_STATE } from '../../../../../../common'; + import mockTransformListRow from '../../../../common/__mocks__/transform_list_row.json'; -import { TransformListRow, isCompletedBatchTransform, TRANSFORM_STATE } from '../../../../common'; +import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; describe('Transform: isCompletedBatchTransform()', () => { test('isCompletedBatchTransform()', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/common.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/common.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index e480381d6fb8e..0e9b531e1feaf 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -19,7 +19,7 @@ import { PreviewRequestBody, TransformPivotConfig, } from '../../../../common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; import { transformTableFactory } from './transform_table'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx similarity index 99% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 6bea4e5750d2d..9c2da53c36d6b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -21,15 +21,15 @@ import { Direction, } from '@elastic/eui'; +import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; + import { OnTableChangeArg, SortDirection, SORT_DIRECTION } from '../../../../../shared_imports'; import { useRefreshTransformList, - TransformId, TransformListRow, TRANSFORM_MODE, TRANSFORM_LIST_COLUMN, - TRANSFORM_STATE, } from '../../../../common'; import { AuthorizationContext } from '../../../../lib/authorization'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx similarity index 95% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx index dd2369e199d0d..5f05d08734efe 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx @@ -6,8 +6,12 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TRANSFORM_MODE, TransformListRow } from '../../../../common'; + import { StatsBar, TransformStatsBarStats } from '../stats_bar'; -import { TRANSFORM_STATE, TRANSFORM_MODE, TransformListRow } from '../../../../common'; function createTranformStats(transformsList: TransformListRow[]) { const transformStats = { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/index.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx similarity index 98% rename from x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 8c174098fb623..002c418b3d2e3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -22,8 +22,9 @@ import { } from '@elastic/eui'; import { APP_GET_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; -import { useDocumentationLinks } from '../../app_dependencies'; + import { useRefreshTransformList, TransformListRow } from '../../common'; +import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useGetTransforms } from '../../hooks'; import { RedirectToCreateTransform } from '../../common/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/plugins/transform/public/app/services/navigation/breadcrumb.ts similarity index 91% rename from x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts rename to x-pack/plugins/transform/public/app/services/navigation/breadcrumb.ts index aa8041a1cbe23..6637b8a39cd56 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts +++ b/x-pack/plugins/transform/public/app/services/navigation/breadcrumb.ts @@ -7,12 +7,11 @@ import { textService } from '../text'; import { linkToHome } from './links'; -import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; +import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public'; type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; export enum BREADCRUMB_SECTION { - MANAGEMENT = 'management', HOME = 'home', CLONE_TRANSFORM = 'cloneTransform', CREATE_TRANSFORM = 'createTransform', @@ -29,7 +28,6 @@ type Breadcrumbs = { class BreadcrumbService { private breadcrumbs: Breadcrumbs = { - management: [], home: [], cloneTransform: [], createTransform: [], @@ -41,7 +39,6 @@ class BreadcrumbService { // Home and sections this.breadcrumbs.home = [ - ...this.breadcrumbs.management, { text: textService.breadcrumbs.home, href: linkToHome(), diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/doc_title.ts b/x-pack/plugins/transform/public/app/services/navigation/doc_title.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/services/navigation/doc_title.ts rename to x-pack/plugins/transform/public/app/services/navigation/doc_title.ts diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/index.ts b/x-pack/plugins/transform/public/app/services/navigation/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/services/navigation/index.ts rename to x-pack/plugins/transform/public/app/services/navigation/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/links.ts b/x-pack/plugins/transform/public/app/services/navigation/links.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/services/navigation/links.ts rename to x-pack/plugins/transform/public/app/services/navigation/links.ts diff --git a/x-pack/legacy/plugins/transform/public/app/services/text/index.ts b/x-pack/plugins/transform/public/app/services/text/index.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/services/text/index.ts rename to x-pack/plugins/transform/public/app/services/text/index.ts diff --git a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts b/x-pack/plugins/transform/public/app/services/text/text.ts similarity index 100% rename from x-pack/legacy/plugins/transform/public/app/services/text/text.ts rename to x-pack/plugins/transform/public/app/services/text/text.ts diff --git a/x-pack/plugins/transform/public/index.ts b/x-pack/plugins/transform/public/index.ts new file mode 100644 index 0000000000000..83b5addbc5b2c --- /dev/null +++ b/x-pack/plugins/transform/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import './app/index.scss'; +import { TransformUiPlugin } from './plugin'; + +/** @public */ +export const plugin = () => { + return new TransformUiPlugin(); +}; diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts new file mode 100644 index 0000000000000..1a34e7a641365 --- /dev/null +++ b/x-pack/plugins/transform/public/plugin.ts @@ -0,0 +1,70 @@ +/* + * 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 as kbnI18n } from '@kbn/i18n'; + +import { CoreSetup } from 'src/core/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { ManagementSetup } from 'src/plugins/management/public'; + +import { renderApp } from './app/app'; +import { AppDependencies } from './app/app_dependencies'; +import { breadcrumbService } from './app/services/navigation'; +import { docTitleService } from './app/services/navigation'; +import { textService } from './app/services/text'; + +export interface PluginsDependencies { + data: DataPublicPluginStart; + management: ManagementSetup; +} + +export class TransformUiPlugin { + public setup(coreSetup: CoreSetup<PluginsDependencies>, pluginsSetup: PluginsDependencies): void { + const { management } = pluginsSetup; + + // Register management section + const esSection = management.sections.getSection('elasticsearch'); + if (esSection !== undefined) { + esSection.registerApp({ + id: 'transform', + title: kbnI18n.translate('xpack.transform.appTitle', { + defaultMessage: 'Transforms', + }), + order: 3, + mount: async ({ element, setBreadcrumbs }) => { + const { http, notifications, getStartServices } = coreSetup; + const startServices = await getStartServices(); + const [core, plugins] = startServices; + const { chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; + const { data } = plugins; + const { docTitle } = chrome; + + // Initialize services + textService.init(); + docTitleService.init(docTitle.change); + breadcrumbService.setup(setBreadcrumbs); + + // AppCore/AppPlugins to be passed on as React context + const appDependencies: AppDependencies = { + chrome, + data, + docLinks, + http, + i18n, + notifications, + overlays, + savedObjects, + uiSettings, + }; + + return renderApp(element, appDependencies); + }, + }); + } + } + + public start() {} + public stop() {} +} diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts similarity index 56% rename from x-pack/legacy/plugins/transform/public/shared_imports.ts rename to x-pack/plugins/transform/public/shared_imports.ts index 1ca71f8c4aa77..3582dd5d266e2 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; -export { XJsonMode } from '../../../../plugins/es_ui_shared/console_lang/ace/modes/x_json'; +export { createSavedSearchesLoader } from '../../../../src/plugins/discover/public'; +export { XJsonMode } from '../../es_ui_shared/console_lang/ace/modes/x_json'; export { collapseLiteralStrings, expandLiteralStrings, -} from '../../../../../src/plugins/es_ui_shared/console_lang/lib'; +} from '../../../../src/plugins/es_ui_shared/console_lang/lib'; export { SendRequestConfig, @@ -17,12 +17,12 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; +} from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { CronEditor, DAY, -} from '../../../../../src/plugins/es_ui_shared/public/components/cron_editor'; +} from '../../../../src/plugins/es_ui_shared/public/components/cron_editor'; // Custom version of EuiInMemoryTable with TypeScript // support and a fix for updating sorting props. @@ -37,10 +37,10 @@ export { SortingPropType, SortDirection, SORT_DIRECTION, -} from '../../ml/public/application/components/ml_in_memory_table'; +} from '../../../legacy/plugins/ml/public/application/components/ml_in_memory_table'; // Needs to be imported because we're reusing KqlFilterBar which depends on it. -export { setDependencyCache } from '../../ml/public/application/util/dependency_cache'; +export { setDependencyCache } from '../../../legacy/plugins/ml/public/application/util/dependency_cache'; // @ts-ignore: could not find declaration file for module -export { KqlFilterBar } from '../../ml/public/application/components/kql_filter_bar'; +export { KqlFilterBar } from '../../../legacy/plugins/ml/public/application/components/kql_filter_bar'; diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index d09152bf1a603..295375794c04e 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -10,10 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ResponseError, CustomHttpResponseOptions } from 'src/core/server'; -import { - TransformEndpointRequest, - TransformEndpointResult, -} from '../../../../../legacy/plugins/transform/public/app/hooks/use_api_types'; +import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; const REQUEST_TIMEOUT = 'RequestTimeout'; diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts index 6003a88ffa40c..9d7fb16ecc19a 100644 --- a/x-pack/plugins/transform/server/routes/api/privileges.ts +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -3,13 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - APP_CLUSTER_PRIVILEGES, - APP_INDEX_PRIVILEGES, -} from '../../../../../legacy/plugins/transform/common/constants'; -// NOTE: now we import it from our "public" folder, but when the Authorisation lib -// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder -import { Privileges } from '../../../../../legacy/plugins/transform/public/app/lib/authorization'; +import { APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES } from '../../../common/constants'; +import { Privileges } from '../../../common'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 7aaae1f1c7039..bf201323a3c2f 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -9,12 +9,12 @@ import { RequestHandler } from 'kibana/server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; -import { TRANSFORM_STATE } from '../../../../../legacy/plugins/transform/public/app/common'; import { TransformEndpointRequest, TransformEndpointResult, -} from '../../../../../legacy/plugins/transform/public/app/hooks/use_api_types'; -import { TransformId } from '../../../../../legacy/plugins/transform/public/app/common/transform'; + TransformId, + TRANSFORM_STATE, +} from '../../../common'; import { RouteDependencies } from '../../types'; @@ -273,9 +273,7 @@ const previewTransformHandler: RequestHandler = async (ctx, req, res) => { }; const startTransformsHandler: RequestHandler = async (ctx, req, res) => { - const { transformsInfo } = req.body as { - transformsInfo: TransformEndpointRequest[]; - }; + const transformsInfo = req.body as TransformEndpointRequest[]; try { return res.ok({ @@ -313,9 +311,7 @@ async function startTransforms( } const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { - const { transformsInfo } = req.body as { - transformsInfo: TransformEndpointRequest[]; - }; + const transformsInfo = req.body as TransformEndpointRequest[]; try { return res.ok({ diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index 422fdec7ab77e..722a3f52376b4 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessage } from '../../../../../legacy/plugins/transform/common/types/messages'; +import { AuditMessage } from '../../../common/types/messages'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { RouteDependencies } from '../../types'; diff --git a/x-pack/plugins/transform/server/routes/index.ts b/x-pack/plugins/transform/server/routes/index.ts index 953490920cbcb..07c21e58e64e4 100644 --- a/x-pack/plugins/transform/server/routes/index.ts +++ b/x-pack/plugins/transform/server/routes/index.ts @@ -9,7 +9,7 @@ import { RouteDependencies } from '../types'; import { registerPrivilegesRoute } from './api/privileges'; import { registerTransformsRoutes } from './api/transforms'; -import { API_BASE_PATH } from '../../../../legacy/plugins/transform/common/constants'; +import { API_BASE_PATH } from '../../common/constants'; export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index cf66883412edb..6883faa5ee230 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": false, "ui": true, + "optionalPlugins": ["alerting", "alertingBuiltins"], "requiredPlugins": ["management", "charts", "data"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss new file mode 100644 index 0000000000000..d0a7039ae24e1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss @@ -0,0 +1,3 @@ +.actAlertVisualization__chart { + height: $euiSize * 15; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 9a01a7f50c3df..2bf779e550618 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -20,6 +20,8 @@ import { EuiComboBoxOptionOption, EuiFormRow, EuiCallOut, + EuiEmptyPrompt, + EuiText, } from '@elastic/eui'; import { COMPARATORS, builtInComparators } from '../../../../common/constants'; import { @@ -39,6 +41,7 @@ import { import { builtInAggregationTypes } from '../../../../common/constants'; import { IndexThresholdAlertParams } from './types'; import { AlertsContextValue } from '../../../context/alerts_context'; +import './expression.scss'; const DEFAULT_VALUES = { AGGREGATION_TYPE: 'count', @@ -63,6 +66,7 @@ const expressionFieldsWithValidation = [ interface IndexThresholdProps { alertParams: IndexThresholdAlertParams; + alertInterval: string; setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: { [key: string]: string[] }; @@ -71,6 +75,7 @@ interface IndexThresholdProps { export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThresholdProps> = ({ alertParams, + alertInterval, setAlertParams, setAlertProperty, errors, @@ -451,6 +456,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR} threshold={threshold} errors={errors} + popupPosition={'upLeft'} onChangeSelectedThreshold={selectedThresholds => setAlertParams('threshold', selectedThresholds) } @@ -461,6 +467,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr </EuiFlexItem> <EuiFlexItem grow={false}> <ForLastExpression + popupPosition={'upLeft'} timeWindowSize={timeWindowSize || 1} timeWindowUnit={timeWindowUnit || ''} errors={errors} @@ -473,16 +480,35 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr /> </EuiFlexItem> </EuiFlexGroup> - {canShowVizualization ? null : ( - <Fragment> - <ThresholdVisualization - alertParams={alertParams} - aggregationTypes={builtInAggregationTypes} - comparators={builtInComparators} - alertsContext={alertsContext} - /> - </Fragment> - )} + <EuiSpacer size="l" /> + <div className="actAlertVisualization__chart"> + {canShowVizualization ? ( + <Fragment> + <EuiSpacer size="xl" /> + <EuiEmptyPrompt + iconType="visBarVertical" + body={ + <EuiText color="subdued"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription" + defaultMessage="Complete the expression above to generate a preview" + /> + </EuiText> + } + /> + </Fragment> + ) : ( + <Fragment> + <ThresholdVisualization + alertParams={alertParams} + alertInterval={alertInterval} + aggregationTypes={builtInAggregationTypes} + comparators={builtInComparators} + alertsContext={alertsContext} + /> + </Fragment> + )} + </div> </Fragment> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts index 956007049a821..943e0e5d7b835 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/api.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { HttpSetup } from 'kibana/public'; +import { TimeSeriesResult } from '../types'; +export { TimeSeriesResult } from '../types'; const WATCHER_API_ROOT = '/api/watcher'; @@ -60,20 +62,35 @@ export const loadIndexPatterns = async () => { return savedObjects; }; +const TimeSeriesQueryRoute = '/api/alerting_builtins/index_threshold/_time_series_query'; + +interface GetThresholdAlertVisualizationDataParams { + model: any; + visualizeOptions: any; + http: HttpSetup; +} + export async function getThresholdAlertVisualizationData({ model, visualizeOptions, http, -}: { - model: any; - visualizeOptions: any; - http: HttpSetup; -}): Promise<Record<string, any>> { - const { visualizeData } = await http.post(`${WATCHER_API_ROOT}/watch/visualize`, { - body: JSON.stringify({ - watch: model, - options: visualizeOptions, - }), +}: GetThresholdAlertVisualizationDataParams): Promise<TimeSeriesResult> { + const timeSeriesQueryParams = { + index: model.index, + timeField: model.timeField, + aggType: model.aggType, + aggField: model.aggField, + groupBy: model.groupBy, + termField: model.termField, + termSize: model.termSize, + timeWindowSize: model.timeWindowSize, + timeWindowUnit: model.timeWindowUnit, + dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), + dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), + interval: visualizeOptions.interval, + }; + + return await http.post<TimeSeriesResult>(TimeSeriesQueryRoute, { + body: JSON.stringify(timeSeriesQueryParams), }); - return visualizeData; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts deleted file mode 100644 index 34e435be152f6..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.test.ts +++ /dev/null @@ -1,123 +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 moment from 'moment'; - -import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; - -describe('calcAutoIntervalNear', () => { - test('1h/0 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalNear(0, Number(moment.duration(1, 'h'))); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('undefined/100 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalNear(0, undefined as any); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('1ms/100 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'ms'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('200ms/100 buckets = 2ms buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(200, 'ms'))); - expect(interval.asMilliseconds()).toBe(2); - }); - - test('1s/1000 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 's'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('1000h/1000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1000, 'hours'))); - expect(interval.asHours()).toBe(1); - }); - - test('1h/100 buckets = 30s buckets', () => { - const interval = calcAutoIntervalNear(100, Number(moment.duration(1, 'hours'))); - expect(interval.asSeconds()).toBe(30); - }); - - test('1d/25 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(25, Number(moment.duration(1, 'day'))); - expect(interval.asHours()).toBe(1); - }); - - test('1y/1000 buckets = 12h buckets', () => { - const interval = calcAutoIntervalNear(1000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(12); - }); - - test('1y/10000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalNear(10000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(1); - }); - - test('1y/100000 buckets = 5m buckets', () => { - const interval = calcAutoIntervalNear(100000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(5); - }); -}); - -describe('calcAutoIntervalLessThan', () => { - test('1h/0 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalLessThan(0, Number(moment.duration(1, 'h'))); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('undefined/100 buckets = 0ms buckets', () => { - const interval = calcAutoIntervalLessThan(0, undefined as any); - expect(interval.asMilliseconds()).toBe(0); - }); - - test('1ms/100 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'ms'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('200ms/100 buckets = 2ms buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(200, 'ms'))); - expect(interval.asMilliseconds()).toBe(2); - }); - - test('1s/1000 buckets = 1ms buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 's'))); - expect(interval.asMilliseconds()).toBe(1); - }); - - test('1000h/1000 buckets = 1h buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1000, 'hours'))); - expect(interval.asHours()).toBe(1); - }); - - test('1h/100 buckets = 30s buckets', () => { - const interval = calcAutoIntervalLessThan(100, Number(moment.duration(1, 'hours'))); - expect(interval.asSeconds()).toBe(30); - }); - - test('1d/25 buckets = 30m buckets', () => { - const interval = calcAutoIntervalLessThan(25, Number(moment.duration(1, 'day'))); - expect(interval.asMinutes()).toBe(30); - }); - - test('1y/1000 buckets = 3h buckets', () => { - const interval = calcAutoIntervalLessThan(1000, Number(moment.duration(1, 'year'))); - expect(interval.asHours()).toBe(3); - }); - - test('1y/10000 buckets = 30m buckets', () => { - const interval = calcAutoIntervalLessThan(10000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(30); - }); - - test('1y/100000 buckets = 5m buckets', () => { - const interval = calcAutoIntervalLessThan(100000, Number(moment.duration(1, 'year'))); - expect(interval.asMinutes()).toBe(5); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts deleted file mode 100644 index c910f1e6752d4..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_auto_interval.ts +++ /dev/null @@ -1,132 +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 moment from 'moment'; - -const boundsDescending = [ - { - bound: Infinity, - interval: Number(moment.duration(1, 'year')), - }, - { - bound: Number(moment.duration(1, 'year')), - interval: Number(moment.duration(1, 'month')), - }, - { - bound: Number(moment.duration(3, 'week')), - interval: Number(moment.duration(1, 'week')), - }, - { - bound: Number(moment.duration(1, 'week')), - interval: Number(moment.duration(1, 'd')), - }, - { - bound: Number(moment.duration(24, 'hour')), - interval: Number(moment.duration(12, 'hour')), - }, - { - bound: Number(moment.duration(6, 'hour')), - interval: Number(moment.duration(3, 'hour')), - }, - { - bound: Number(moment.duration(2, 'hour')), - interval: Number(moment.duration(1, 'hour')), - }, - { - bound: Number(moment.duration(45, 'minute')), - interval: Number(moment.duration(30, 'minute')), - }, - { - bound: Number(moment.duration(20, 'minute')), - interval: Number(moment.duration(10, 'minute')), - }, - { - bound: Number(moment.duration(9, 'minute')), - interval: Number(moment.duration(5, 'minute')), - }, - { - bound: Number(moment.duration(3, 'minute')), - interval: Number(moment.duration(1, 'minute')), - }, - { - bound: Number(moment.duration(45, 'second')), - interval: Number(moment.duration(30, 'second')), - }, - { - bound: Number(moment.duration(15, 'second')), - interval: Number(moment.duration(10, 'second')), - }, - { - bound: Number(moment.duration(7.5, 'second')), - interval: Number(moment.duration(5, 'second')), - }, - { - bound: Number(moment.duration(5, 'second')), - interval: Number(moment.duration(1, 'second')), - }, - { - bound: Number(moment.duration(500, 'ms')), - interval: Number(moment.duration(100, 'ms')), - }, -]; - -function getPerBucketMs(count: number, duration: number) { - const ms = duration / count; - return isFinite(ms) ? ms : NaN; -} - -function normalizeMinimumInterval(targetMs: number) { - const value = isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1); - return moment.duration(value); -} - -/** - * Using some simple rules we pick a "pretty" interval that will - * produce around the number of buckets desired given a time range. - * - * @param targetBucketCount desired number of buckets - * @param duration time range the agg covers - */ -export function calcAutoIntervalNear(targetBucketCount: number, duration: number) { - const targetPerBucketMs = getPerBucketMs(targetBucketCount, duration); - - // Find the first bound which is smaller than our target. - const lowerBoundIndex = boundsDescending.findIndex(({ bound }) => { - const boundMs = Number(bound); - return boundMs <= targetPerBucketMs; - }); - - // The bound immediately preceeding that lower bound contains the - // interval most closely matching our target. - if (lowerBoundIndex !== -1) { - const nearestInterval = boundsDescending[lowerBoundIndex - 1].interval; - return moment.duration(nearestInterval); - } - - // If the target is smaller than any of our bounds, then we'll use it for the interval as-is. - return normalizeMinimumInterval(targetPerBucketMs); -} - -/** - * Pick a "pretty" interval that produces no more than the maxBucketCount - * for the given time range. - * - * @param maxBucketCount maximum number of buckets to create - * @param duration amount of time covered by the agg - */ -export function calcAutoIntervalLessThan(maxBucketCount: number, duration: number) { - const maxPerBucketMs = getPerBucketMs(maxBucketCount, duration); - - for (const { interval } of boundsDescending) { - // Find the highest interval which meets our per bucket limitation. - if (interval <= maxPerBucketMs) { - return moment.duration(interval); - } - } - - // If the max is smaller than any of our intervals, then we'll use it for the interval as-is. - return normalizeMinimumInterval(maxPerBucketMs); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js deleted file mode 100644 index bb5725c567b1f..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/calc_es_interval.js +++ /dev/null @@ -1,58 +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 dateMath from '@elastic/datemath'; - -import { parseEsInterval } from '../../../../../../../../../../src/plugins/data/public'; - -const unitsDesc = dateMath.unitsDesc; -const largeMax = unitsDesc.indexOf('M'); - -/** - * Convert a moment.duration into an es - * compatible expression, and provide - * associated metadata - * - * @param {moment.duration} duration - * @return {object} - */ -export function convertDurationToNormalizedEsInterval(duration) { - for (let i = 0; i < unitsDesc.length; i++) { - const unit = unitsDesc[i]; - const val = duration.as(unit); - // find a unit that rounds neatly - if (val >= 1 && Math.floor(val) === val) { - // if the unit is "large", like years, but - // isn't set to 1 ES will puke. So keep going until - // we get out of the "large" units - if (i <= largeMax && val !== 1) { - continue; - } - - return { - value: val, - unit: unit, - expression: val + unit, - }; - } - } - - const ms = duration.as('ms'); - return { - value: ms, - unit: 'ms', - expression: ms + 'ms', - }; -} - -export function convertIntervalToEsInterval(interval) { - const { value, unit } = parseEsInterval(interval); - return { - value, - unit, - expression: interval, - }; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js deleted file mode 100644 index f49e85ddefea8..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/lib/time_buckets/time_buckets.js +++ /dev/null @@ -1,397 +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 _ from 'lodash'; -import moment from 'moment'; -import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval'; -import { - convertDurationToNormalizedEsInterval, - convertIntervalToEsInterval, -} from './calc_es_interval'; -import { fieldFormats, parseInterval } from '../../../../../../../../../../src/plugins/data/public'; - -function isValidMoment(m) { - return m && 'isValid' in m && m.isValid(); -} - -/** - * Helper class for wrapping the concept of an "Interval", - * which describes a timespan that will separate moments. - * - * @param {state} object - one of "" - * @param {[type]} display [description] - */ -function TimeBuckets(uiSettings, dataFieldsFormats) { - this.uiSettings = uiSettings; - this.dataFieldsFormats = dataFieldsFormats; - return TimeBuckets.__cached__(this); -} - -/**** - * PUBLIC API - ****/ - -/** - * Set the bounds that these buckets are expected to cover. - * This is required to support interval "auto" as well - * as interval scaling. - * - * @param {object} input - an object with properties min and max, - * representing the edges for the time span - * we should cover - * - * @returns {undefined} - */ -TimeBuckets.prototype.setBounds = function(input) { - if (!input) return this.clearBounds(); - - let bounds; - if (_.isPlainObject(input)) { - // accept the response from timefilter.getActiveBounds() - bounds = [input.min, input.max]; - } else { - bounds = Array.isArray(input) ? input : []; - } - - const moments = _(bounds) - .map(_.ary(moment, 1)) - .sortBy(Number); - - const valid = moments.size() === 2 && moments.every(isValidMoment); - if (!valid) { - this.clearBounds(); - throw new Error('invalid bounds set: ' + input); - } - - this._lb = moments.shift(); - this._ub = moments.pop(); - if (this.getDuration().asSeconds() < 0) { - throw new TypeError('Intervals must be positive'); - } -}; - -/** - * Clear the stored bounds - * - * @return {undefined} - */ -TimeBuckets.prototype.clearBounds = function() { - this._lb = this._ub = null; -}; - -/** - * Check to see if we have received bounds yet - * - * @return {Boolean} - */ -TimeBuckets.prototype.hasBounds = function() { - return isValidMoment(this._ub) && isValidMoment(this._lb); -}; - -/** - * Return the current bounds, if we have any. - * - * THIS DOES NOT CLONE THE BOUNDS, so editing them - * may have unexpected side-effects. Always - * call bounds.min.clone() before editing - * - * @return {object|undefined} - If bounds are not defined, this - * returns undefined, else it returns the bounds - * for these buckets. This object has two props, - * min and max. Each property will be a moment() - * object - * - */ -TimeBuckets.prototype.getBounds = function() { - if (!this.hasBounds()) return; - return { - min: this._lb, - max: this._ub, - }; -}; - -/** - * Get a moment duration object representing - * the distance between the bounds, if the bounds - * are set. - * - * @return {moment.duration|undefined} - */ -TimeBuckets.prototype.getDuration = function() { - if (!this.hasBounds()) return; - return moment.duration(this._ub - this._lb, 'ms'); -}; - -/** - * Update the interval at which buckets should be - * generated. - * - * Input can be one of the following: - * - Any object from src/legacy/ui/agg_types/buckets/_interval_options.js - * - "auto" - * - Pass a valid moment unit - * - a moment.duration object. - * - * @param {object|string|moment.duration} input - see desc - */ -TimeBuckets.prototype.setInterval = function(input) { - // Preserve the original units because they're lost when the interval is converted to a - // moment duration object. - this.originalInterval = input; - - let interval = input; - - // selection object -> val - if (_.isObject(input)) { - interval = input.val; - } - - if (!interval || interval === 'auto') { - this._i = 'auto'; - return; - } - - if (_.isString(interval)) { - input = interval; - interval = parseInterval(interval); - if (+interval === 0) { - interval = null; - } - } - - // if the value wasn't converted to a duration, and isn't - // already a duration, we have a problem - if (!moment.isDuration(interval)) { - throw new TypeError('"' + input + '" is not a valid interval.'); - } - - this._i = interval; -}; - -/** - * Get the interval for the buckets. If the - * number of buckets created by the interval set - * is larger than config:histogram:maxBars then the - * interval will be scaled up. If the number of buckets - * created is less than one, the interval is scaled back. - * - * The interval object returned is a moment.duration - * object that has been decorated with the following - * properties. - * - * interval.description: a text description of the interval. - * designed to be used list "field per {{ desc }}". - * - "minute" - * - "10 days" - * - "3 years" - * - * interval.expr: the elasticsearch expression that creates this - * interval. If the interval does not properly form an elasticsearch - * expression it will be forced into one. - * - * interval.scaled: the interval was adjusted to - * accommodate the maxBars setting. - * - * interval.scale: the number that y-values should be - * multiplied by - * - * interval.scaleDescription: a description that reflects - * the values which will be produced by using the - * interval.scale. - * - * - * @return {[type]} [description] - */ -TimeBuckets.prototype.getInterval = function(useNormalizedEsInterval = true) { - const self = this; - const duration = self.getDuration(); - const parsedInterval = readInterval(); - - if (useNormalizedEsInterval) { - return decorateInterval(maybeScaleInterval(parsedInterval)); - } else { - return decorateInterval(parsedInterval); - } - - // either pull the interval from state or calculate the auto-interval - function readInterval() { - const interval = self._i; - if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(self.uiSettings.get('histogram:barTarget'), Number(duration)); - } - - // check to see if the interval should be scaled, and scale it if so - function maybeScaleInterval(interval) { - if (!self.hasBounds()) return interval; - - const maxLength = self.uiSettings.get('histogram:maxBars'); - const approxLen = duration / interval; - let scaled; - - if (approxLen > maxLength) { - scaled = calcAutoIntervalLessThan(maxLength, Number(duration)); - } else { - return interval; - } - - if (+scaled === +interval) return interval; - - decorateInterval(interval); - return _.assign(scaled, { - preScaled: interval, - scale: interval / scaled, - scaled: true, - }); - } - - // append some TimeBuckets specific props to the interval - function decorateInterval(interval) { - const esInterval = useNormalizedEsInterval - ? convertDurationToNormalizedEsInterval(interval) - : convertIntervalToEsInterval(self.originalInterval); - interval.esValue = esInterval.value; - interval.esUnit = esInterval.unit; - interval.expression = esInterval.expression; - interval.overflow = duration > interval ? moment.duration(interval - duration) : false; - - const prettyUnits = moment.normalizeUnits(esInterval.unit); - if (esInterval.value === 1) { - interval.description = prettyUnits; - } else { - interval.description = esInterval.value + ' ' + prettyUnits + 's'; - } - - return interval; - } -}; - -/** - * Get a date format string that will represent dates that - * progress at our interval. - * - * Since our interval can be as small as 1ms, the default - * date format is usually way too much. with `dateFormat:scaled` - * users can modify how dates are formatted within series - * produced by TimeBuckets - * - * @return {string} - */ -TimeBuckets.prototype.getScaledDateFormat = function() { - const interval = this.getInterval(); - const rules = this.uiSettings.get('dateFormat:scaled'); - - for (let i = rules.length - 1; i >= 0; i--) { - const rule = rules[i]; - if (!rule[0] || interval >= moment.duration(rule[0])) { - return rule[1]; - } - } - - return this.uiSettings.get('dateFormat'); -}; - -TimeBuckets.prototype.getScaledDateFormatter = function() { - const fieldFormatsService = this.dataFieldsFormats; - const DateFieldFormat = fieldFormatsService.getType(fieldFormats.FIELD_FORMAT_IDS.DATE); - - return new DateFieldFormat( - { - pattern: this.getScaledDateFormat(), - }, - configPath => this.uiSettings.get(configPath) - ); -}; - -TimeBuckets.__cached__ = function(self) { - let cache = {}; - const sameMoment = same(moment.isMoment); - const sameDuration = same(moment.isDuration); - - const desc = { - __cached__: { - value: self, - }, - }; - - const breakers = { - setBounds: 'bounds', - clearBounds: 'bounds', - setInterval: 'interval', - }; - - const resources = { - bounds: { - setup: function() { - return [self._lb, self._ub]; - }, - changes: function(prev) { - return !sameMoment(prev[0], self._lb) || !sameMoment(prev[1], self._ub); - }, - }, - interval: { - setup: function() { - return self._i; - }, - changes: function(prev) { - return !sameDuration(prev, this._i); - }, - }, - }; - - function cachedGetter(prop) { - return { - value: function cachedGetter(...rest) { - if (cache.hasOwnProperty(prop)) { - return cache[prop]; - } - - return (cache[prop] = self[prop](...rest)); - }, - }; - } - - function cacheBreaker(prop) { - const resource = resources[breakers[prop]]; - const setup = resource.setup; - const changes = resource.changes; - const fn = self[prop]; - - return { - value: function cacheBreaker() { - const prev = setup.call(self); - const ret = fn.apply(self, arguments); - - if (changes.call(self, prev)) { - cache = {}; - } - - return ret; - }, - }; - } - - function same(checkType) { - return function(a, b) { - if (a === b) return true; - if (checkType(a) === checkType(b)) return +a === +b; - return false; - }; - } - - _.forOwn(TimeBuckets.prototype, function(fn, prop) { - if (prop[0] === '_') return; - - if (breakers.hasOwnProperty(prop)) { - desc[prop] = cacheBreaker(prop); - } else { - desc[prop] = cachedGetter(prop); - } - }); - - return Object.create(self, desc); -}; - -export { TimeBuckets }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts index 356b0fbbc0845..d5b64f1489b8d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +export { + TimeSeriesResult, + TimeSeriesResultRow, + MetricResult, +} from '../../../../../../alerting_builtins/common/alert_types/index_threshold'; + export interface Comparator { text: string; value: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 4d97a59e36320..f27e35fe7609d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment, useEffect, useState } from 'react'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AnnotationDomainTypes, @@ -18,17 +18,16 @@ import { Position, ScaleType, Settings, + niceTimeFormatter, } from '@elastic/charts'; -import dateMath from '@elastic/datemath'; import moment from 'moment-timezone'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getThresholdAlertVisualizationData } from './lib/api'; import { AggregationType, Comparator } from '../../../../common/types'; -/* TODO: This file was copied from ui/time_buckets for NP migration. We should clean this up and add TS support */ -import { TimeBuckets } from './lib/time_buckets'; import { AlertsContextValue } from '../../../context/alerts_context'; import { IndexThresholdAlertParams } from './types'; +import { parseDuration } from '../../../../../../alerting/common/parse_duration'; const customTheme = () => { return { @@ -60,35 +59,26 @@ const getTimezone = (uiSettings: IUiSettingsClient) => { return tzOffset; }; -const getDomain = (alertParams: any) => { - const VISUALIZE_TIME_WINDOW_MULTIPLIER = 5; - const fromExpression = `now-${alertParams.timeWindowSize * VISUALIZE_TIME_WINDOW_MULTIPLIER}${ - alertParams.timeWindowUnit - }`; - const toExpression = 'now'; - const fromMoment = dateMath.parse(fromExpression); - const toMoment = dateMath.parse(toExpression); - const visualizeTimeWindowFrom = fromMoment ? fromMoment.valueOf() : 0; - const visualizeTimeWindowTo = toMoment ? toMoment.valueOf() : 0; +const getDomain = (alertInterval: string) => { + const VISUALIZE_INTERVALS = 30; + let intervalMillis: number; + + try { + intervalMillis = parseDuration(alertInterval); + } catch (err) { + intervalMillis = 1000 * 60; // default to one minute if not parseable + } + + const now = Date.now(); return { - min: visualizeTimeWindowFrom, - max: visualizeTimeWindowTo, + min: now - intervalMillis * VISUALIZE_INTERVALS, + max: now, }; }; -const getTimeBuckets = ( - uiSettings: IUiSettingsClient, - dataFieldsFormats: any, - alertParams: any -) => { - const domain = getDomain(alertParams); - const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); - timeBuckets.setBounds(domain); - return timeBuckets; -}; - interface Props { alertParams: IndexThresholdAlertParams; + alertInterval: string; aggregationTypes: { [key: string]: AggregationType }; comparators: { [key: string]: Comparator; @@ -96,8 +86,10 @@ interface Props { alertsContext: AlertsContextValue; } +type MetricResult = [number, number]; // [epochMillis, value] export const ThresholdVisualization: React.FunctionComponent<Props> = ({ alertParams, + alertInterval, aggregationTypes, comparators, alertsContext, @@ -119,18 +111,14 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<undefined | any>(undefined); - const [visualizationData, setVisualizationData] = useState<Record<string, any>>([]); + const [visualizationData, setVisualizationData] = useState<Record<string, MetricResult[]>>(); useEffect(() => { (async () => { try { setIsLoading(true); setVisualizationData( - await getThresholdAlertVisualizationData({ - model: alertWithoutActions, - visualizeOptions, - http, - }) + await getVisualizationData(alertWithoutActions, visualizeOptions, http) ); } catch (e) { if (toastNotifications) { @@ -167,15 +155,11 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ } const chartsTheme = charts.theme.useChartsTheme(); - const domain = getDomain(alertParams); - const timeBuckets = new TimeBuckets(uiSettings, dataFieldsFormats); - timeBuckets.setBounds(domain); - const interval = timeBuckets.getInterval().expression; + const domain = getDomain(alertInterval); const visualizeOptions = { - rangeFrom: domain.min, - rangeTo: domain.max, - interval, - timezone: getTimezone(uiSettings), + rangeFrom: new Date(domain.min).toISOString(), + rangeTo: new Date(domain.max).toISOString(), + interval: alertInterval, }; // Fetching visualization data is independent of alert actions @@ -237,15 +221,10 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ } }); }); - const dateFormatter = (d: number) => { - return moment(d) - .tz(timezone) - .format(getTimeBuckets(uiSettings, dataFieldsFormats, alertParams).getScaledDateFormat()); - }; + const dateFormatter = niceTimeFormatter([domain.min, domain.max]); const aggLabel = aggregationTypes[aggType].text; return ( <div data-test-subj="alertVisualizationChart"> - <EuiSpacer size="l" /> {alertVisualizationDataKeys.length ? ( <Chart size={['100%', 200]} renderer="canvas"> <Settings @@ -309,10 +288,28 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ /> </EuiCallOut> )} - <EuiSpacer size="l" /> </div> ); } return null; }; + +// convert the data from the visualization API into something easier to digest with charts +async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) { + const vizData = await getThresholdAlertVisualizationData({ + model, + visualizeOptions, + http, + }); + const result: Record<string, Array<[number, number]>> = {}; + + for (const groupMetrics of vizData.results) { + result[groupMetrics.group] = groupMetrics.metrics.map(metricResult => [ + Date.parse(metricResult[0]), + metricResult[1], + ]); + } + + return result; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss deleted file mode 100644 index b5755bc35b1c1..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss +++ /dev/null @@ -1,7 +0,0 @@ -.actConnectorModal { - z-index: 9000; -} - -.euiComboBoxOptionsList { - z-index: 10000; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.scss new file mode 100644 index 0000000000000..f8fa882cd617d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.scss @@ -0,0 +1,3 @@ +.actConnectorModal { + z-index: 9000; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 1cc26f39990ff..fcf3a309a1fc9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -23,6 +23,7 @@ import { ActionType, ActionConnector, IErrorObject, ActionTypeModel } from '../. import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; import { TypeRegistry } from '../../type_registry'; +import './connector_add_modal.scss'; interface ConnectorAddModalProps { actionType: ActionType; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/_index.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/_index.scss deleted file mode 100644 index 98c6c2a307a74..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'actions_connectors_list'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 4e514281be0ea..9444b31a8b78f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -25,6 +25,7 @@ import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_ import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities'; import { DeleteConnectorsModal } from '../../../components/delete_connectors_modal'; import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; +import './actions_connectors_list.scss'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 190f14f0428d8..240e05c3a6e43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -210,13 +210,13 @@ export const AlertForm = ({ {AlertParamsExpressionComponent ? ( <AlertParamsExpressionComponent alertParams={alert.params} + alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`} errors={errors} setAlertParams={setAlertParams} setAlertProperty={setAlertProperty} alertsContext={alertsContext} /> ) : null} - <EuiSpacer size="xl" /> {defaultActionGroupId ? ( <ActionForm actions={alert.actions} diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a0c12154988a1..ceb3a6dd60166 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -28,6 +28,19 @@ export enum ReindexStatus { } export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation'; + +export interface QueueSettings extends SavedObjectAttributes { + queuedAt: number; +} + +export interface ReindexOptions extends SavedObjectAttributes { + /** + * Set this key to configure a reindex operation as part of a + * batch to be run in series. + */ + queueSettings?: QueueSettings; +} + export interface ReindexOperation extends SavedObjectAttributes { indexName: string; newIndexName: string; @@ -40,6 +53,15 @@ export interface ReindexOperation extends SavedObjectAttributes { // This field is only used for the singleton IndexConsumerType documents. runningReindexCount: number | null; + + /** + * Options for the reindexing strategy. + * + * @remark + * Marked as optional for backwards compatibility. We should still + * be able to handle older ReindexOperation objects. + */ + reindexOptions?: ReindexOptions; } export type ReindexSavedObject = SavedObject<ReindexOperation>; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts index e7636eea66479..6182a82f6f1bd 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts @@ -90,9 +90,9 @@ export const esVersionCheck = async ( } }; -export const versionCheckHandlerWrapper = (handler: RequestHandler<any, any, any>) => async ( +export const versionCheckHandlerWrapper = <P, Q, B>(handler: RequestHandler<P, Q, B>) => async ( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest<P, Q, B>, response: KibanaResponseFactory ) => { const errorResponse = await esVersionCheck(ctx, response); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts index b7bc197fbd162..59922abd3e635 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error.ts @@ -12,6 +12,7 @@ import { ReindexTaskFailed, ReindexAlreadyInProgress, MultipleReindexJobsFound, + ReindexCannotBeCancelled, } from './error_symbols'; export class ReindexError extends Error { @@ -32,4 +33,5 @@ export const error = { reindexTaskCannotBeDeleted: createErrorFactory(ReindexTaskCannotBeDeleted), reindexAlreadyInProgress: createErrorFactory(ReindexAlreadyInProgress), multipleReindexJobsFound: createErrorFactory(MultipleReindexJobsFound), + reindexCannotBeCancelled: createErrorFactory(ReindexCannotBeCancelled), }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts index 9e49d280d1be2..d5e8d643f4595 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/error_symbols.ts @@ -11,5 +11,6 @@ export const CannotCreateIndex = Symbol('CannotCreateIndex'); export const ReindexTaskFailed = Symbol('ReindexTaskFailed'); export const ReindexTaskCannotBeDeleted = Symbol('ReindexTaskCannotBeDeleted'); export const ReindexAlreadyInProgress = Symbol('ReindexAlreadyInProgress'); +export const ReindexCannotBeCancelled = Symbol('ReindexCannotBeCancelled'); export const MultipleReindexJobsFound = Symbol('MultipleReindexJobsFound'); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/op_utils.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/op_utils.ts new file mode 100644 index 0000000000000..dbed7de13f010 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/op_utils.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { flow } from 'fp-ts/lib/function'; +import { ReindexSavedObject } from '../../../common/types'; + +export interface SortedReindexSavedObjects { + /** + * Reindex objects sorted into this array represent Elasticsearch reindex tasks that + * have no inherent order and are considered to be processed in parallel. + */ + parallel: ReindexSavedObject[]; + + /** + * Reindex objects sorted into this array represent Elasticsearch reindex tasks that + * are consistently ordered (see {@link orderQueuedReindexOperations}) and should be + * processed in order. + */ + queue: ReindexSavedObject[]; +} + +const sortReindexOperations = (ops: ReindexSavedObject[]): SortedReindexSavedObjects => { + const parallel: ReindexSavedObject[] = []; + const queue: ReindexSavedObject[] = []; + for (const op of ops) { + if (op.attributes.reindexOptions?.queueSettings) { + queue.push(op); + } else { + parallel.push(op); + } + } + + return { + parallel, + queue, + }; +}; +const orderQueuedReindexOperations = ({ + parallel, + queue, +}: SortedReindexSavedObjects): SortedReindexSavedObjects => ({ + parallel, + // Sort asc + queue: queue.sort( + (a, b) => + a.attributes.reindexOptions!.queueSettings!.queuedAt - + b.attributes.reindexOptions!.queueSettings!.queuedAt + ), +}); + +export const sortAndOrderReindexOperations = flow( + sortReindexOperations, + orderQueuedReindexOperations +); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 2ae340f12d80c..422e78c2f12ad 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -11,6 +11,7 @@ import { IndexGroup, REINDEX_OP_TYPE, ReindexOperation, + ReindexOptions, ReindexSavedObject, ReindexStatus, ReindexStep, @@ -34,8 +35,9 @@ export interface ReindexActions { /** * Creates a new reindexOp, does not perform any pre-flight checks. * @param indexName + * @param opts Options for the reindex operation */ - createReindexOp(indexName: string): Promise<ReindexSavedObject>; + createReindexOp(indexName: string, opts?: ReindexOptions): Promise<ReindexSavedObject>; /** * Deletes a reindexOp. @@ -150,7 +152,7 @@ export const reindexActionsFactory = ( // ----- Public interface return { - async createReindexOp(indexName: string) { + async createReindexOp(indexName: string, opts?: ReindexOptions) { return client.create<ReindexOperation>(REINDEX_OP_TYPE, { indexName, newIndexName: generateNewIndexName(indexName), @@ -161,6 +163,7 @@ export const reindexActionsFactory = ( reindexTaskPercComplete: null, errorMessage: null, runningReindexCount: null, + reindexOptions: opts, }); }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 6c3b2c869dc7f..886ea6761e3b7 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -215,7 +215,7 @@ describe('reindexService', () => { await service.createReindexOperation('myIndex'); - expect(actions.createReindexOp).toHaveBeenCalledWith('myIndex'); + expect(actions.createReindexOp).toHaveBeenCalledWith('myIndex', undefined); }); it('fails if index does not exist', async () => { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index b274743bdf279..aa91b925b744b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -8,6 +8,7 @@ import { first } from 'rxjs/operators'; import { IndexGroup, + ReindexOptions, ReindexSavedObject, ReindexStatus, ReindexStep, @@ -51,8 +52,9 @@ export interface ReindexService { /** * Creates a new reindex operation for a given index. * @param indexName + * @param opts */ - createReindexOperation(indexName: string): Promise<ReindexSavedObject>; + createReindexOperation(indexName: string, opts?: ReindexOptions): Promise<ReindexSavedObject>; /** * Retrieves all reindex operations that have the given status. @@ -83,8 +85,9 @@ export interface ReindexService { /** * Resumes the paused reindex operation for a given index. * @param indexName + * @param opts As with {@link createReindexOperation} we support this setting. */ - resumeReindexOperation(indexName: string): Promise<ReindexSavedObject>; + resumeReindexOperation(indexName: string, opts?: ReindexOptions): Promise<ReindexSavedObject>; /** * Cancel an in-progress reindex operation for a given index. Only allowed when the @@ -517,7 +520,7 @@ export const reindexServiceFactory = ( } }, - async createReindexOperation(indexName: string) { + async createReindexOperation(indexName: string, opts?: ReindexOptions) { const indexExists = await callAsUser('indices.exists', { index: indexName }); if (!indexExists) { throw error.indexNotFound(`Index ${indexName} does not exist in this cluster.`); @@ -539,7 +542,7 @@ export const reindexServiceFactory = ( } } - return actions.createReindexOp(indexName); + return actions.createReindexOp(indexName, opts); }, async findReindexOperation(indexName: string) { @@ -627,7 +630,7 @@ export const reindexServiceFactory = ( }); }, - async resumeReindexOperation(indexName: string) { + async resumeReindexOperation(indexName: string, opts?: ReindexOptions) { const reindexOp = await this.findReindexOperation(indexName); if (!reindexOp) { @@ -642,7 +645,10 @@ export const reindexServiceFactory = ( throw new Error(`Reindex operation must be paused in order to be resumed.`); } - return actions.updateReindexOp(op, { status: ReindexStatus.inProgress }); + return actions.updateReindexOp(op, { + status: ReindexStatus.inProgress, + reindexOptions: opts, + }); }); }, @@ -650,11 +656,13 @@ export const reindexServiceFactory = ( const reindexOp = await this.findReindexOperation(indexName); if (!reindexOp) { - throw new Error(`No reindex operation found for index ${indexName}`); + throw error.indexNotFound(`No reindex operation found for index ${indexName}`); } else if (reindexOp.attributes.status !== ReindexStatus.inProgress) { - throw new Error(`Reindex operation is not in progress`); + throw error.reindexCannotBeCancelled(`Reindex operation is not in progress`); } else if (reindexOp.attributes.lastCompletedStep !== ReindexStep.reindexStarted) { - throw new Error(`Reindex operation is not current waiting for reindex task to complete`); + throw error.reindexCannotBeCancelled( + `Reindex operation is not currently waiting for reindex task to complete` + ); } const resp = await callAsUser('tasks.cancel', { @@ -662,7 +670,7 @@ export const reindexServiceFactory = ( }); if (resp.node_failures && resp.node_failures.length > 0) { - throw new Error(`Could not cancel reindex.`); + throw error.reindexCannotBeCancelled(`Could not cancel reindex.`); } return reindexOp; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index bad6db62efe41..482b9f280ad7e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -5,12 +5,12 @@ */ import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; - import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; import { ReindexService, reindexServiceFactory } from './reindex_service'; import { LicensingPluginSetup } from '../../../../licensing/server'; +import { sortAndOrderReindexOperations } from './op_utils'; const POLL_INTERVAL = 30000; // If no nodes have been able to update this index in 2 minutes (due to missing credentials), set to paused. @@ -105,15 +105,17 @@ export class ReindexWorker { private startUpdateOperationLoop = async () => { this.updateOperationLoopRunning = true; - while (this.inProgressOps.length > 0) { - this.log.debug(`Updating ${this.inProgressOps.length} reindex operations`); + try { + while (this.inProgressOps.length > 0) { + this.log.debug(`Updating ${this.inProgressOps.length} reindex operations`); - // Push each operation through the state machine and refresh. - await Promise.all(this.inProgressOps.map(this.processNextStep)); - await this.refresh(); + // Push each operation through the state machine and refresh. + await Promise.all(this.inProgressOps.map(this.processNextStep)); + await this.refresh(); + } + } finally { + this.updateOperationLoopRunning = false; } - - this.updateOperationLoopRunning = false; }; private pollForOperations = async () => { @@ -126,14 +128,28 @@ export class ReindexWorker { } }; - private refresh = async () => { + private updateInProgressOps = async () => { try { - this.inProgressOps = await this.reindexService.findAllByStatus(ReindexStatus.inProgress); + const inProgressOps = await this.reindexService.findAllByStatus(ReindexStatus.inProgress); + const { parallel, queue } = sortAndOrderReindexOperations(inProgressOps); + + const [firstOpInQueue] = queue; + + if (firstOpInQueue) { + this.log.debug( + `Queue detected; current length ${queue.length}, current item ReindexOperation(id: ${firstOpInQueue.id}, indexName: ${firstOpInQueue.attributes.indexName})` + ); + } + + this.inProgressOps = parallel.concat(firstOpInQueue ? [firstOpInQueue] : []); } catch (e) { - this.log.debug(`Could not fetch reindex operations from Elasticsearch`); + this.log.debug(`Could not fetch reindex operations from Elasticsearch, ${e.message}`); this.inProgressOps = []; } + }; + private refresh = async () => { + await this.updateInProgressOps(); // If there are operations in progress and we're not already updating operations, kick off the update loop if (!this.updateOperationLoopRunning) { this.startUpdateOperationLoop(); diff --git a/x-pack/legacy/plugins/transform/public/app/services/ui_metric/index.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts similarity index 73% rename from x-pack/legacy/plugins/transform/public/app/services/ui_metric/index.ts rename to x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts index e7c3f961824e3..9f1d3e4021c3f 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/ui_metric/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts @@ -3,4 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { uiMetricService } from './ui_metric'; + +export { createReindexWorker, registerReindexIndicesRoutes } from './reindex_indices'; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts new file mode 100644 index 0000000000000..944b4a225d442 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { IScopedClusterClient, Logger, SavedObjectsClientContract } from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../../licensing/server'; + +import { ReindexOperation, ReindexOptions, ReindexStatus } from '../../../common/types'; + +import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; +import { reindexServiceFactory } from '../../lib/reindexing'; +import { CredentialStore } from '../../lib/reindexing/credential_store'; +import { error } from '../../lib/reindexing/error'; + +interface ReindexHandlerArgs { + savedObjects: SavedObjectsClientContract; + dataClient: IScopedClusterClient; + indexName: string; + log: Logger; + licensing: LicensingPluginSetup; + headers: Record<string, any>; + credentialStore: CredentialStore; + enqueue?: boolean; +} + +export const reindexHandler = async ({ + credentialStore, + dataClient, + headers, + indexName, + licensing, + log, + savedObjects, + enqueue, +}: ReindexHandlerArgs): Promise<ReindexOperation> => { + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(savedObjects, callAsCurrentUser); + const reindexService = reindexServiceFactory(callAsCurrentUser, reindexActions, log, licensing); + + if (!(await reindexService.hasRequiredPrivileges(indexName))) { + throw error.accessForbidden( + i18n.translate('xpack.upgradeAssistant.reindex.reindexPrivilegesErrorBatch', { + defaultMessage: `You do not have adequate privileges to reindex "{indexName}".`, + values: { indexName }, + }) + ); + } + + const existingOp = await reindexService.findReindexOperation(indexName); + + const opts: ReindexOptions | undefined = enqueue + ? { queueSettings: { queuedAt: Date.now() } } + : undefined; + + // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. + const reindexOp = + existingOp && existingOp.attributes.status === ReindexStatus.paused + ? await reindexService.resumeReindexOperation(indexName, opts) + : await reindexService.createReindexOperation(indexName, opts); + + // Add users credentials for the worker to use + credentialStore.set(reindexOp, headers); + + return reindexOp.attributes; +}; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts similarity index 70% rename from x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts index 695bb6304cfdf..af4f7f436ec81 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts @@ -5,9 +5,9 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { licensingMock } from '../../../licensing/server/mocks'; -import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; -import { createRequestMock } from './__mocks__/request.mock'; +import { licensingMock } from '../../../../licensing/server/mocks'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from '../__mocks__/routes.mock'; +import { createRequestMock } from '../__mocks__/request.mock'; const mockReindexService = { hasRequiredPrivileges: jest.fn(), @@ -21,18 +21,23 @@ const mockReindexService = { cancelReindexing: jest.fn(), }; -jest.mock('../lib/es_version_precheck', () => ({ +jest.mock('../../lib/es_version_precheck', () => ({ versionCheckHandlerWrapper: (a: any) => a, })); -jest.mock('../lib/reindexing', () => { +jest.mock('../../lib/reindexing', () => { return { reindexServiceFactory: () => mockReindexService, }; }); -import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexWarning } from '../../common/types'; -import { credentialStoreFactory } from '../lib/reindexing/credential_store'; +import { + IndexGroup, + ReindexSavedObject, + ReindexStatus, + ReindexWarning, +} from '../../../common/types'; +import { credentialStoreFactory } from '../../lib/reindexing/credential_store'; import { registerReindexIndicesRoutes } from './reindex_indices'; /** @@ -76,7 +81,7 @@ describe('reindex API', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('GET /api/upgrade_assistant/reindex/{indexName}', () => { @@ -161,7 +166,7 @@ describe('reindex API', () => { ); // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex'); + expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex', undefined); // It returned the right results expect(resp.status).toEqual(200); @@ -228,7 +233,7 @@ describe('reindex API', () => { kibanaResponseFactory ); // It called resume correctly - expect(mockReindexService.resumeReindexOperation).toHaveBeenCalledWith('theIndex'); + expect(mockReindexService.resumeReindexOperation).toHaveBeenCalledWith('theIndex', undefined); expect(mockReindexService.createReindexOperation).not.toHaveBeenCalled(); // It returned the right results @@ -255,6 +260,111 @@ describe('reindex API', () => { }); }); + describe('POST /api/upgrade_assistant/reindex/batch', () => { + const queueSettingsArg = { + queueSettings: { queuedAt: expect.any(Number) }, + }; + it('creates a collection of index operations', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex2' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex3' }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex2', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 3, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [], + enqueued: [ + { indexName: 'theIndex1' }, + { indexName: 'theIndex2' }, + { indexName: 'theIndex3' }, + ], + }); + }); + + it('gracefully handles partial successes', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockRejectedValueOnce(new Error('oops!')); + + mockReindexService.hasRequiredPrivileges + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenCalledTimes(2); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [ + { + indexName: 'theIndex2', + message: 'You do not have adequate privileges to reindex "theIndex2".', + }, + { indexName: 'theIndex3', message: 'oops!' }, + ], + enqueued: [{ indexName: 'theIndex1' }], + }); + }); + }); + describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => { it('returns a 501', async () => { mockReindexService.cancelReindexing.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts similarity index 58% rename from x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts rename to x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 72c2f2c29b72e..697b73d8e10f6 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -3,31 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { schema } from '@kbn/config-schema'; import { - Logger, ElasticsearchServiceSetup, - SavedObjectsClient, kibanaResponseFactory, -} from '../../../../../src/core/server'; -import { ReindexStatus } from '../../common/types'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; -import { CredentialStore } from '../lib/reindexing/credential_store'; -import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; -import { RouteDependencies } from '../types'; -import { LicensingPluginSetup } from '../../../licensing/server'; -import { ReindexError } from '../lib/reindexing/error'; + Logger, + SavedObjectsClient, +} from '../../../../../../src/core/server'; + +import { LicensingPluginSetup } from '../../../../licensing/server'; + +import { ReindexStatus } from '../../../common/types'; + +import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; +import { reindexServiceFactory, ReindexWorker } from '../../lib/reindexing'; +import { CredentialStore } from '../../lib/reindexing/credential_store'; +import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; +import { sortAndOrderReindexOperations } from '../../lib/reindexing/op_utils'; +import { ReindexError } from '../../lib/reindexing/error'; +import { RouteDependencies } from '../../types'; import { AccessForbidden, - IndexNotFound, CannotCreateIndex, + IndexNotFound, + MultipleReindexJobsFound, ReindexAlreadyInProgress, + ReindexCannotBeCancelled, ReindexTaskCannotBeDeleted, ReindexTaskFailed, - MultipleReindexJobsFound, -} from '../lib/reindexing/error_symbols'; +} from '../../lib/reindexing/error_symbols'; + +import { reindexHandler } from './reindex_handler'; +import { GetBatchQueueResponse, PostBatchResponse } from './types'; interface CreateReindexWorker { logger: Logger; @@ -63,6 +70,7 @@ const mapAnyErrorToKibanaHttpResponse = (e: any) => { return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); case ReindexAlreadyInProgress: case MultipleReindexJobsFound: + case ReindexCannotBeCancelled: return kibanaResponseFactory.badRequest({ body: e.message }); default: // nothing matched @@ -91,46 +99,31 @@ export function registerReindexIndicesRoutes( async ( { core: { - savedObjects, + savedObjects: { client: savedObjectsClient }, elasticsearch: { dataClient }, }, }, request, response ) => { - const { indexName } = request.params as any; - const { client } = savedObjects; - const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); - const reindexActions = reindexActionsFactory(client, callAsCurrentUser); - const reindexService = reindexServiceFactory( - callAsCurrentUser, - reindexActions, - log, - licensing - ); - + const { indexName } = request.params; try { - if (!(await reindexService.hasRequiredPrivileges(indexName))) { - return response.forbidden({ - body: `You do not have adequate privileges to reindex this index.`, - }); - } - - const existingOp = await reindexService.findReindexOperation(indexName); - - // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. - const reindexOp = - existingOp && existingOp.attributes.status === ReindexStatus.paused - ? await reindexService.resumeReindexOperation(indexName) - : await reindexService.createReindexOperation(indexName); - - // Add users credentials for the worker to use - credentialStore.set(reindexOp, request.headers); + const result = await reindexHandler({ + savedObjects: savedObjectsClient, + dataClient, + indexName, + log, + licensing, + headers: request.headers, + credentialStore, + }); // Kick the worker on this node to immediately pickup the new reindex operation. getWorker().forceRefresh(); - return response.ok({ body: reindexOp.attributes }); + return response.ok({ + body: result, + }); } catch (e) { return mapAnyErrorToKibanaHttpResponse(e); } @@ -138,6 +131,97 @@ export function registerReindexIndicesRoutes( ) ); + // Get the current batch queue + router.get( + { + path: `${BASE_PATH}/batch/queue`, + validate: {}, + }, + async ( + { + core: { + elasticsearch: { dataClient }, + savedObjects, + }, + }, + request, + response + ) => { + const { client } = savedObjects; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + try { + const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); + const { queue } = sortAndOrderReindexOperations(inProgressOps); + const result: GetBatchQueueResponse = { + queue: queue.map(savedObject => savedObject.attributes), + }; + return response.ok({ + body: result, + }); + } catch (e) { + return mapAnyErrorToKibanaHttpResponse(e); + } + } + ); + + // Add indices for reindexing to the worker's batch + router.post( + { + path: `${BASE_PATH}/batch`, + validate: { + body: schema.object({ + indexNames: schema.arrayOf(schema.string()), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { indexNames } = request.body; + const results: PostBatchResponse = { + enqueued: [], + errors: [], + }; + for (const indexName of indexNames) { + try { + const result = await reindexHandler({ + savedObjects: savedObjectsClient, + dataClient, + indexName, + log, + licensing, + headers: request.headers, + credentialStore, + enqueue: true, + }); + results.enqueued.push(result); + } catch (e) { + results.errors.push({ + indexName, + message: e.message, + }); + } + } + + if (results.errors.length < indexNames.length) { + // Kick the worker on this node to immediately pickup the batch. + getWorker().forceRefresh(); + } + + return response.ok({ body: results }); + } + ) + ); + // Get status router.get( { @@ -160,7 +244,7 @@ export function registerReindexIndicesRoutes( response ) => { const { client } = savedObjects; - const { indexName } = request.params as any; + const { indexName } = request.params; const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); const reindexActions = reindexActionsFactory(client, callAsCurrentUser); const reindexService = reindexServiceFactory( @@ -215,7 +299,7 @@ export function registerReindexIndicesRoutes( request, response ) => { - const { indexName } = request.params as any; + const { indexName } = request.params; const { client } = savedObjects; const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); const reindexActions = reindexActionsFactory(client, callAsCurrentUser); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/types.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/types.ts new file mode 100644 index 0000000000000..251450a9e37f2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReindexOperation } from '../../../common/types'; + +// These types represent contracts from the reindex RESTful API endpoints and +// should be changed in a way that respects backwards compatibility. + +export interface PostBatchResponse { + enqueued: ReindexOperation[]; + errors: Array<{ indexName: string; message: string }>; +} + +export interface GetBatchQueueResponse { + queue: ReindexOperation[]; +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index aacf0b8f87ed0..1aa0f8e2c9f16 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -111,7 +111,7 @@ export default function indexTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [index]: types that failed validation:\n- [index.0]: expected value of type [string] but got [number]\n- [index.1]: expected value to equal [null] but got [666]', + 'error validating action type config: [index]: types that failed validation:\n- [index.0]: expected value of type [string] but got [number]\n- [index.1]: expected value to equal [null]', }); }); }); diff --git a/x-pack/test/api_integration/apis/endpoint/alerts.ts b/x-pack/test/api_integration/apis/endpoint/alerts.ts index 45ae2b5438e18..1890b6f5d557d 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts.ts @@ -70,7 +70,7 @@ export default function({ getService }: FtrProviderContext) { .get('/api/endpoint/alerts?page_size=0') .set('kbn-xsrf', 'xxx') .expect(400); - expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [1]'); + expect(body.message).to.contain('Value must be equal to or greater than [1]'); }); it('alerts api should return links to the next and previous pages using cursor-based pagination', async () => { diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 4b0cc8d93a395..516891d84dc91 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -100,7 +100,7 @@ export default function({ getService }: FtrProviderContext) { ], }) .expect(400); - expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [1]'); + expect(body.message).to.contain('Value must be equal to or greater than [1]'); }); it('metadata api should return page based on filters passed.', async () => { diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index e1a435e000fae..cbf44e07ac40a 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -78,7 +78,7 @@ export default function({ getService }) { uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( - '[request query.meta_fields]: could not parse array value from [stringValue]' + '[request query.meta_fields]: could not parse array value from json input' ); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts index 2a9824f46778d..6af27d558432d 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts @@ -165,7 +165,8 @@ export default function({ getService }: FtrProviderContext) { }, ]; - describe('job on data set with date_nanos time field', function() { + // test failures, see #59419 + describe.skip('job on data set with date_nanos time field', function() { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/event_rate_nanos'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index a13cf3d61128e..66b2f00009b18 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -271,7 +271,8 @@ export default function({ getService }: FtrProviderContext) { }, ]; - describe('saved search', function() { + // test failures, see #59354 + describe.skip('saved search', function() { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/farequote'); diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index b931a5cb0ca63..e0b1ec544d460 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -15,7 +15,10 @@ export default function enterSpaceFunctonalTests({ describe('Enter Space', function() { this.tags('smoke'); - before(async () => await esArchiver.load('spaces/enter_space')); + before(async () => { + await esArchiver.load('spaces/enter_space'); + await PageObjects.security.forceLogout(); + }); after(async () => await esArchiver.unload('spaces/enter_space')); afterEach(async () => { diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 5af9bc135ae27..7b4a1e6e2b8a0 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -23,7 +23,10 @@ export default function spaceSelectorFunctonalTests({ describe('Spaces', function() { this.tags('smoke'); describe('Space Selector', () => { - before(async () => await esArchiver.load('spaces/selector')); + before(async () => { + await esArchiver.load('spaces/selector'); + await PageObjects.security.forceLogout(); + }); after(async () => await esArchiver.unload('spaces/selector')); afterEach(async () => { diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index f06dc0a14a383..5f05fdb093d10 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../legacy/plugins/transform/public/app/common'; +import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; function getTransformConfig(): TransformPivotConfig { const date = Date.now(); diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz index dd8f719305bb9..0788e40326bb3 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json index 725a58af99325..fa5d6447762be 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/api_feature/mappings.json @@ -3,7 +3,7 @@ "value": { "aliases": { }, - "index": "my-index", + "index": "events-endpoint-1", "mappings": { "_meta": { "version": "1.5.0-dev" @@ -5262,4 +5262,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 8de5b5e69d34d..323c01e234880 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -5,7 +5,7 @@ */ export function MonitoringPageProvider({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'header', 'shield', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'header', 'security', 'shield', 'spaceSelector']); const testSubjects = getService('testSubjects'); const security = getService('security'); @@ -19,7 +19,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { }); if (!useSuperUser) { - await PageObjects.common.navigateToApp('login'); + await PageObjects.security.forceLogout(); await PageObjects.shield.login('basic_monitoring_user', 'monitoring_user_password'); } await PageObjects.common.navigateToApp('monitoring'); diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 4803596b973bc..4b097b916573d 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -110,12 +110,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } await userMenu.clickLogoutButton(); - - await retry.waitForWithTimeout( - 'login form', - config.get('timeouts.waitFor') * 5, - async () => await find.existsByDisplayedByCssSelector('.login-form') - ); + await this.waitForLoginForm(); } async forceLogout() { @@ -129,11 +124,17 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const url = PageObjects.common.getHostPort() + '/logout'; await browser.get(url); log.debug('Waiting on the login form to appear'); - await retry.waitForWithTimeout( - 'login form', - config.get('timeouts.waitFor') * 5, - async () => await find.existsByDisplayedByCssSelector('.login-form') - ); + await this.waitForLoginForm(); + } + + async waitForLoginForm() { + await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => { + const alert = await browser.getAlert(); + if (alert && alert.accept) { + await alert.accept(); + } + return await find.existsByDisplayedByCssSelector('.login-form'); + }); } async clickRolesSection() { diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts index 6a4a1dfff6ea1..94792d6590caa 100644 --- a/x-pack/test/functional/services/transform_ui/api.ts +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common'; import { - TRANSFORM_STATE, TransformPivotConfig, TransformStats, -} from '../../../../legacy/plugins/transform/public/app/common'; +} from '../../../../plugins/transform/public/app/common'; export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); diff --git a/x-pack/test/siem_cypress/config.ts b/x-pack/test/siem_cypress/config.ts index 05c1e471e74a9..261cfdc1c4429 100644 --- a/x-pack/test/siem_cypress/config.ts +++ b/x-pack/test/siem_cypress/config.ts @@ -8,6 +8,8 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; + import { SiemCypressTestRunner } from './runner'; export default async function({ readConfigFile }: FtrConfigProviderContext) { @@ -32,6 +34,8 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), // define custom es server here + 'xpack.security.authc.api_key.enabled=true', + 'xpack.security.enabled=true', ], }, @@ -41,6 +45,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false', // define custom kibana server args here + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, ], }, }; diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 38fc1f0c6356f..a99c02ffef23e 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { ReindexStatus, REINDEX_OP_TYPE } from '../../../plugins/upgrade_assistant/common/types'; +import { generateNewIndexName } from '../../../plugins/upgrade_assistant/server/lib/reindexing/index_settings'; export default function({ getService }) { const supertest = getService('supertest'); @@ -134,5 +135,73 @@ export default function({ getService }) { expect(lastState.errorMessage).to.equal(null); expect(lastState.status).to.equal(ReindexStatus.completed); }); + + it('should reindex a batch in order and report queue state', async () => { + const assertQueueState = async (firstInQueueIndexName, queueLength) => { + const response = await supertest + .get(`/api/upgrade_assistant/reindex/batch/queue`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { queue } = response.body; + + const [firstInQueue] = queue; + + if (!firstInQueueIndexName) { + expect(firstInQueueIndexName).to.be(undefined); + } else { + expect(firstInQueue.indexName).to.be(firstInQueueIndexName); + } + + expect(queue.length).to.be(queueLength); + }; + + const test1 = 'batch-reindex-test1'; + const test2 = 'batch-reindex-test2'; + const test3 = 'batch-reindex-test3'; + + const cleanupReindex = async indexName => { + try { + await es.indices.delete({ index: generateNewIndexName(indexName) }); + } catch (e) { + try { + await es.indices.delete({ index: indexName }); + } catch (e) { + // Ignore + } + } + }; + + try { + // Set up indices for the batch + await es.indices.create({ index: test1 }); + await es.indices.create({ index: test2 }); + await es.indices.create({ index: test3 }); + + const result = await supertest + .post(`/api/upgrade_assistant/reindex/batch`) + .set('kbn-xsrf', 'xxx') + .send({ indexNames: [test1, test2, test3] }) + .expect(200); + + expect(result.body.enqueued.length).to.equal(3); + expect(result.body.errors.length).to.equal(0); + + await assertQueueState(test1, 3); + await waitForReindexToComplete(test1); + + await assertQueueState(test2, 2); + await waitForReindexToComplete(test2); + + await assertQueueState(test3, 1); + await waitForReindexToComplete(test3); + + await assertQueueState(undefined, 0); + } finally { + await cleanupReindex(test1); + await cleanupReindex(test2); + await cleanupReindex(test3); + } + }); }); }