diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.md b/docs/development/core/public/kibana-plugin-public.savedobject.md index b1bb3e267bf0e..542d7d3a063ec 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObject +export interface SavedObject ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md index 5a08b3f97f429..97772112ff006 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBatchResponse +export interface SavedObjectsBatchResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md index c479e9f9f3e3f..ef83acb8fd73d 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md index ca0eabb265901..a57702c305f50 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkupdateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateObject +export interface SavedObjectsBulkUpdateObject ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md index 391b2f57205d5..c5072bea5b50e 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md @@ -9,5 +9,5 @@ Creates multiple documents at once Signature: ```typescript -bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; +bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md index a54dfe72167a7..37b9f6d6c951d 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md @@ -12,7 +12,7 @@ Returns an array of objects by id bulkGet: (objects?: { id: string; type: string; - }[]) => Promise>; + }[]) => Promise>; ``` ## Example diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md index 94ae9bb70ccda..2f8565def3d40 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Update multiple documents at once Signature: ```typescript -bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; +bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; ``` ## Parameters @@ -20,7 +20,7 @@ bulkUpdate(objects?: SavedObjectsBulkUpdateObje Returns: -`Promise>` +`Promise>` The result of the update operation containing both failed and updated saved objects. diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md index 5a7666084ea0f..ea3fbe31ca8a5 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md @@ -9,5 +9,5 @@ Persists an object Signature: ```typescript -create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; +create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index d3494045952ad..3d8005c9390b7 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md index bddbadd3e1361..37a91f7211da5 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md @@ -9,5 +9,5 @@ Fetches a single object Signature: ```typescript -get: (type: string, id: string) => Promise>; +get: (type: string, id: string) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 7aa17eae2da87..5c22a2a0bdd91 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,12 +20,12 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<SavedObjectAttributes>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Creates multiple documents at once | -| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | -| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | +| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<unknown>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<unknown>> | Creates multiple documents at once | +| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<unknown>> | Returns an array of objects by id | +| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T = unknown>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | -| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T = unknown>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T = unknown>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md index 9f7e46943bbd5..d1049a75edf1f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md @@ -9,7 +9,7 @@ Updates an object Signature: ```typescript -update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md index f60e7305fba34..31ab73464dfd9 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md @@ -11,7 +11,7 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md index 1f6de163ec17d..4906d5967f7df 100644 --- a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md @@ -11,7 +11,7 @@ It provides basic functionality for creating/saving/deleting saved objects, but Signature: ```typescript -export declare class SimpleSavedObject +export declare class SimpleSavedObject ``` ## Constructors diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md new file mode 100644 index 0000000000000..55e5cf74fc512 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [(constructor)](./kibana-plugin-server.elasticsearchconfig._constructor_.md) + +## ElasticsearchConfig.(constructor) + +Constructs a new instance of the `ElasticsearchConfig` class + +Signature: + +```typescript +constructor(rawConfig: ElasticsearchConfigType); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rawConfig | ElasticsearchConfigType | | + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md new file mode 100644 index 0000000000000..097654f1fb090 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.apiversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [apiVersion](./kibana-plugin-server.elasticsearchconfig.apiversion.md) + +## ElasticsearchConfig.apiVersion property + +Version of the Elasticsearch (6.7, 7.1 or `master`) client will be connecting to. + +Signature: + +```typescript +readonly apiVersion: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md new file mode 100644 index 0000000000000..0b3998e59c5df --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.customheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [customHeaders](./kibana-plugin-server.elasticsearchconfig.customheaders.md) + +## ElasticsearchConfig.customHeaders property + +Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by `requestHeadersWhitelist` configuration. + +Signature: + +```typescript +readonly customHeaders: ElasticsearchConfigType['customHeaders']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md new file mode 100644 index 0000000000000..b5589727d80aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [healthCheckDelay](./kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md) + +## ElasticsearchConfig.healthCheckDelay property + +The interval between health check requests Kibana sends to the Elasticsearch. + +Signature: + +```typescript +readonly healthCheckDelay: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md new file mode 100644 index 0000000000000..29770ba5e0795 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.hosts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [hosts](./kibana-plugin-server.elasticsearchconfig.hosts.md) + +## ElasticsearchConfig.hosts property + +Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. + +Signature: + +```typescript +readonly hosts: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md new file mode 100644 index 0000000000000..42e32f920c1db --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [ignoreVersionMismatch](./kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md) + +## ElasticsearchConfig.ignoreVersionMismatch property + +Whether to allow kibana to connect to a non-compatible elasticsearch node. + +Signature: + +```typescript +readonly ignoreVersionMismatch: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md new file mode 100644 index 0000000000000..64de7f6504450 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.logqueries.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [logQueries](./kibana-plugin-server.elasticsearchconfig.logqueries.md) + +## ElasticsearchConfig.logQueries property + +Specifies whether all queries to the client should be logged (status code, method, query etc.). + +Signature: + +```typescript +readonly logQueries: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md new file mode 100644 index 0000000000000..e478dc7b966a2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) + +## ElasticsearchConfig class + +Wrapper of config schema. + +Signature: + +```typescript +export declare class ElasticsearchConfig +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(rawConfig)](./kibana-plugin-server.elasticsearchconfig._constructor_.md) | | Constructs a new instance of the ElasticsearchConfig class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [apiVersion](./kibana-plugin-server.elasticsearchconfig.apiversion.md) | | string | Version of the Elasticsearch (6.7, 7.1 or master) client will be connecting to. | +| [customHeaders](./kibana-plugin-server.elasticsearchconfig.customheaders.md) | | ElasticsearchConfigType['customHeaders'] | Header names and values to send to Elasticsearch with every request. These headers cannot be overwritten by client-side headers and aren't affected by requestHeadersWhitelist configuration. | +| [healthCheckDelay](./kibana-plugin-server.elasticsearchconfig.healthcheckdelay.md) | | Duration | The interval between health check requests Kibana sends to the Elasticsearch. | +| [hosts](./kibana-plugin-server.elasticsearchconfig.hosts.md) | | string[] | Hosts that the client will connect to. If sniffing is enabled, this list will be used as seeds to discover the rest of your cluster. | +| [ignoreVersionMismatch](./kibana-plugin-server.elasticsearchconfig.ignoreversionmismatch.md) | | boolean | Whether to allow kibana to connect to a non-compatible elasticsearch node. | +| [logQueries](./kibana-plugin-server.elasticsearchconfig.logqueries.md) | | boolean | Specifies whether all queries to the client should be logged (status code, method, query etc.). | +| [password](./kibana-plugin-server.elasticsearchconfig.password.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. | +| [pingTimeout](./kibana-plugin-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | +| [requestHeadersWhitelist](./kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md) | | string[] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | +| [requestTimeout](./kibana-plugin-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | +| [shardTimeout](./kibana-plugin-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | +| [sniffInterval](./kibana-plugin-server.elasticsearchconfig.sniffinterval.md) | | false | Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | +| [sniffOnConnectionFault](./kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | +| [sniffOnStart](./kibana-plugin-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | +| [ssl](./kibana-plugin-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>> & {
certificateAuthorities?: string[];
} | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | +| [username](./kibana-plugin-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. | + diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md new file mode 100644 index 0000000000000..ffe6f75e9874d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.password.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [password](./kibana-plugin-server.elasticsearchconfig.password.md) + +## ElasticsearchConfig.password property + +If Elasticsearch is protected with basic authentication, this setting provides the password that the Kibana server uses to perform its administrative functions. + +Signature: + +```typescript +readonly password?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md new file mode 100644 index 0000000000000..09123f0969b60 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.pingtimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [pingTimeout](./kibana-plugin-server.elasticsearchconfig.pingtimeout.md) + +## ElasticsearchConfig.pingTimeout property + +Timeout after which PING HTTP request will be aborted and retried. + +Signature: + +```typescript +readonly pingTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md new file mode 100644 index 0000000000000..eeced56e3103f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [requestHeadersWhitelist](./kibana-plugin-server.elasticsearchconfig.requestheaderswhitelist.md) + +## ElasticsearchConfig.requestHeadersWhitelist property + +List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. + +Signature: + +```typescript +readonly requestHeadersWhitelist: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md new file mode 100644 index 0000000000000..dbd5ecb939673 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.requesttimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [requestTimeout](./kibana-plugin-server.elasticsearchconfig.requesttimeout.md) + +## ElasticsearchConfig.requestTimeout property + +Timeout after which HTTP request will be aborted and retried. + +Signature: + +```typescript +readonly requestTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md new file mode 100644 index 0000000000000..aa923042bf64f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.shardtimeout.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [shardTimeout](./kibana-plugin-server.elasticsearchconfig.shardtimeout.md) + +## ElasticsearchConfig.shardTimeout property + +Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. + +Signature: + +```typescript +readonly shardTimeout: Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md new file mode 100644 index 0000000000000..37fd2a7439535 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffinterval.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffInterval](./kibana-plugin-server.elasticsearchconfig.sniffinterval.md) + +## ElasticsearchConfig.sniffInterval property + +Interval to perform a sniff operation and make sure the list of nodes is complete. If `false` then sniffing is disabled. + +Signature: + +```typescript +readonly sniffInterval: false | Duration; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md new file mode 100644 index 0000000000000..c703be548d34b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffOnConnectionFault](./kibana-plugin-server.elasticsearchconfig.sniffonconnectionfault.md) + +## ElasticsearchConfig.sniffOnConnectionFault property + +Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. + +Signature: + +```typescript +readonly sniffOnConnectionFault: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md new file mode 100644 index 0000000000000..26a7d9cc11a80 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.sniffonstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [sniffOnStart](./kibana-plugin-server.elasticsearchconfig.sniffonstart.md) + +## ElasticsearchConfig.sniffOnStart property + +Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. + +Signature: + +```typescript +readonly sniffOnStart: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md new file mode 100644 index 0000000000000..4d23c410f59fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.ssl.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [ssl](./kibana-plugin-server.elasticsearchconfig.ssl.md) + +## ElasticsearchConfig.ssl property + +Set of settings configure SSL connection between Kibana and Elasticsearch that are required when `xpack.ssl.verification_mode` in Elasticsearch is set to either `certificate` or `full`. + +Signature: + +```typescript +readonly ssl: Pick> & { + certificateAuthorities?: string[]; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md new file mode 100644 index 0000000000000..d0098d656befb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchconfig.username.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) > [username](./kibana-plugin-server.elasticsearchconfig.username.md) + +## ElasticsearchConfig.username property + +If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. + +Signature: + +```typescript +readonly username?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 482f014b226b9..9ec443d6482e8 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -19,6 +19,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. | +| [ElasticsearchConfig](./kibana-plugin-server.elasticsearchconfig.md) | Wrapper of config schema. | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) | Error to return when the validation is not successful. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.md b/docs/development/core/server/kibana-plugin-server.savedobject.md index b3184fd38ad93..5ab08f4eadf2e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObject +export interface SavedObject ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md index 87386b986009d..7b765055de6c1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkCreateObject +export interface SavedObjectsBulkCreateObject ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md index 20a1194c87eda..2ced4f4c8e1a0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkResponse +export interface SavedObjectsBulkResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md index 8e4e3d761148e..67013290a629d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateobject.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateObject extends Pick +export interface SavedObjectsBulkUpdateObject extends Pick ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md index 065b9df0823cd..3468991611608 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkupdateresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsBulkUpdateResponse +export interface SavedObjectsBulkUpdateResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md index 40f947188de54..47da795631a3a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md @@ -9,7 +9,7 @@ Persists multiple documents batched together as a single request Signature: ```typescript -bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md index c86c30d14db3b..71006e2afa3ee 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md @@ -9,7 +9,7 @@ Returns an array of objects by id Signature: ```typescript -bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md index 33958837ebca3..cceeb9c1ca320 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkupdate.md @@ -9,7 +9,7 @@ Bulk Updates multiple SavedObject at once Signature: ```typescript -bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md index ddb78a57e71bc..7f6fc117937cb 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md @@ -9,7 +9,7 @@ Persists a SavedObject Signature: ```typescript -create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md index f72691d3ce0c8..c8804e2a97851 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md @@ -9,7 +9,7 @@ Find all SavedObjects matching the search query Signature: ```typescript -find(options: SavedObjectsFindOptions): Promise>; +find(options: SavedObjectsFindOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md index 3906462184d4f..b48cef25ca3d1 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md @@ -9,7 +9,7 @@ Retrieves a single object Signature: ```typescript -get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md index 2c71e518b7b05..ed6243c409fa4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md @@ -9,7 +9,7 @@ Updates an SavedObject Signature: ```typescript -update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md index efdc07cea88fd..a79a23db967cc 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md @@ -11,7 +11,7 @@ Return type of the Saved Objects `find()` method. Signature: ```typescript -export interface SavedObjectsFindResponse +export interface SavedObjectsFindResponse ``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md index dfe9e51e62483..f0d8d9edfbe79 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md @@ -9,7 +9,7 @@ Creates multiple documents at once Signature: ```typescript -bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md index 34b113bce5410..e27c5fc3bec9a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md @@ -9,7 +9,7 @@ Returns an array of objects by id Signature: ```typescript -bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md index 23c7a92624957..5ad09d59f4061 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md @@ -9,7 +9,7 @@ Updates multiple objects in bulk Signature: ```typescript -bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md index 29e3c3ab24654..fd6495bd2d3c4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md @@ -9,7 +9,7 @@ Persists an object Signature: ```typescript -create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md index dbf6d59e78d85..ccb9feca1669e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md @@ -7,7 +7,7 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; +find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md index 930a4647ca175..b3ccbc7277b4e 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md @@ -9,7 +9,7 @@ Gets a single object Signature: ```typescript -get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md index 5e9f69ecc567b..bb215cdb97af5 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md @@ -9,7 +9,7 @@ Updates an object Signature: ```typescript -update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; ``` ## Parameters diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md index 64c9037735358..130b0b4faaa07 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> ``` ## Properties diff --git a/package.json b/package.json index b11234b6312e9..c3fe290e7934f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "@babel/core": "^7.5.5", "@babel/register": "^7.7.0", "@elastic/apm-rum": "^4.6.0", - "@elastic/charts": "^17.0.2", + "@elastic/charts": "^17.1.1", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", "@elastic/eui": "19.0.0", @@ -205,7 +205,7 @@ "leaflet.heat": "0.2.0", "less": "^2.7.3", "less-loader": "5.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clonedeep": "^4.5.0", "lru-cache": "4.1.5", "markdown-it": "^10.0.0", 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 73661ef849cf4..66b72096a593d 100644 --- a/packages/kbn-config-schema/src/types/array_type.test.ts +++ b/packages/kbn-config-schema/src/types/array_type.test.ts @@ -85,14 +85,29 @@ test('fails if mixed types of content in array', () => { ); }); -test('returns empty array if input is empty but type has default value', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); +test('fails if sparse content in array', () => { + const type = schema.arrayOf(schema.string()); expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); -test('returns empty array if input is empty even if type is required', () => { - const type = schema.arrayOf(schema.string()); +test('fails if sparse content in array if optional', () => { + const type = schema.arrayOf(schema.maybe(schema.string())); + expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); +}); + +test('fails if sparse content in array if nullable', () => { + const type = schema.arrayOf(schema.nullable(schema.string())); expect(type.validate([])).toEqual([]); + expect(type.validate([null])).toEqual([null]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); test('fails for null values if optional', () => { @@ -102,9 +117,19 @@ test('fails for null values if optional', () => { ); }); +test('returns empty array if input is empty but type has default value', () => { + const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); + expect(type.validate([])).toEqual([]); +}); + +test('returns empty array if input is empty even if type is required', () => { + const type = schema.arrayOf(schema.string()); + expect(type.validate([])).toEqual([]); +}); + test('handles default values for undefined values', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'foo' })); - expect(type.validate([undefined])).toEqual(['foo']); + const type = schema.arrayOf(schema.string(), { defaultValue: ['foo'] }); + expect(type.validate(undefined)).toEqual(['foo']); }); test('array within array', () => { diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts index ad74f375588ad..a0353e8348ddd 100644 --- a/packages/kbn-config-schema/src/types/array_type.ts +++ b/packages/kbn-config-schema/src/types/array_type.ts @@ -31,7 +31,7 @@ export class ArrayType extends Type { let schema = internals .array() .items(type.getSchema().optional()) - .sparse(); + .sparse(false); if (options.minSize !== undefined) { schema = schema.min(options.minSize); @@ -49,6 +49,8 @@ export class ArrayType extends Type { case 'any.required': case 'array.base': return `expected value of type [array] but got [${typeDetect(value)}]`; + case 'array.sparse': + return `sparse array are not allowed`; case 'array.parse': return `could not parse array value from [${value}]`; case 'array.min': diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index d2f0b0c358284..5dede7fbf1aaa 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -11,7 +11,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "@kbn/i18n": "1.0.0", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clone": "^4.5.0", "uuid": "3.3.2" }, diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 9798391d47da4..48777f1d54aaf 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -17,7 +17,6 @@ * under the License. */ -// cache buster - https://github.com/elastic/kibana/issues/58077 - 1 export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts deleted file mode 100644 index 44234acd897dc..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import jestDiff from 'jest-diff'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; - -import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; -import { OptimizerConfig } from './optimizer_config'; - -jest.mock('./get_changes.ts'); -jest.mock('execa'); -expect.addSnapshotSerializer(createAbsolutePathSerializer()); - -jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { - expect(cmd).toBe('git'); - expect(args).toEqual([ - 'log', - '-n', - '1', - '--pretty=format:%H', - '--', - expect.stringContaining('kbn-optimizer'), - ]); - expect(opts).toEqual({ - cwd: REPO_ROOT, - }); - - return { - stdout: '', - }; -}); - -jest.requireMock('./get_changes.ts').getChanges.mockImplementation( - async () => - new Map([ - ['/foo/bar/a', 'modified'], - ['/foo/bar/b', 'modified'], - ['/foo/bar/c', 'deleted'], - ]) -); - -describe('getOptimizerCacheKey()', () => { - it('uses latest commit and changes files to create unique value', async () => { - const config = OptimizerConfig.create({ - repoRoot: REPO_ROOT, - }); - - await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` - Object { - "deletedPaths": Array [ - "/foo/bar/c", - ], - "lastCommit": "", - "modifiedPaths": Object {}, - "workerConfig": Object { - "browserslistEnv": "dev", - "cache": true, - "dist": false, - "optimizerCacheKey": "♻", - "profileWebpack": false, - "repoRoot": , - "watch": false, - }, - } - `); - }); -}); - -describe('diffCacheKey()', () => { - it('returns undefined if values are equal', () => { - expect(diffCacheKey('1', '1')).toBe(undefined); - expect(diffCacheKey(1, 1)).toBe(undefined); - expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); - expect( - diffCacheKey( - { - a: '1', - b: '2', - }, - { - b: '2', - a: '1', - } - ) - ).toBe(undefined); - }); - - it('returns a diff if the values are different', () => { - expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` - "- Expected - + Received - -  Array [ -  \\"1\\", -  \\"2\\", -  Object { - - \\"a\\": \\"b\\", - + \\"b\\": \\"a\\", -  }, -  ]" - `); - expect( - diffCacheKey( - { - a: '1', - b: '1', - }, - { - b: '2', - a: '2', - } - ) - ).toMatchInlineSnapshot(` - "- Expected - + Received - -  Object { - - \\"a\\": \\"1\\", - - \\"b\\": \\"1\\", - + \\"a\\": \\"2\\", - + \\"b\\": \\"2\\", -  }" - `); - }); -}); - -describe('reformatJestDiff()', () => { - it('reformats large jestDiff output to focus on the changed lines', () => { - const diff = jestDiff( - { - a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], - }, - { - b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], - } - ); - - expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` - "- Expected - + Received - -  Object { - - \\"a\\": Array [ - + \\"b\\": Array [ -  \\"1\\", -  \\"1\\", -  ... -  \\"1\\", -  \\"1\\", - - \\"2\\", -  \\"1\\", -  \\"1\\", -  ... -  \\"1\\", -  \\"1\\", - + \\"2\\", -  \\"1\\", -  \\"1\\", -  ..." - `); - }); -}); diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.ts index 3529ffa587f16..af6a8a648d29c 100644 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.ts +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.ts @@ -18,6 +18,8 @@ */ import Path from 'path'; +import Fs from 'fs'; +import { promisify } from 'util'; import Chalk from 'chalk'; import execa from 'execa'; @@ -116,9 +118,10 @@ export function reformatJestDiff(diff: string | null) { export interface OptimizerCacheKey { readonly lastCommit: string | undefined; + readonly bootstrap: string | undefined; readonly workerConfig: WorkerConfig; readonly deletedPaths: string[]; - readonly modifiedPaths: Record; + readonly modifiedTimes: Record; } async function getLastCommit() { @@ -133,21 +136,45 @@ async function getLastCommit() { return stdout.trim() || undefined; } +async function getBootstrapCacheKey() { + try { + return await promisify(Fs.readFile)( + Path.resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), + 'utf8' + ); + } catch (error) { + if (error?.code !== 'ENOENT') { + throw error; + } + return undefined; + } +} + export async function getOptimizerCacheKey(config: OptimizerConfig) { - const changes = Array.from((await getChanges(OPTIMIZER_DIR)).entries()); + const [changes, lastCommit, bootstrap] = await Promise.all([ + getChanges(OPTIMIZER_DIR), + getLastCommit(), + getBootstrapCacheKey(), + ] as const); + + const deletedPaths: string[] = []; + const modifiedPaths: string[] = []; + for (const [path, type] of changes) { + (type === 'deleted' ? deletedPaths : modifiedPaths).push(path); + } const cacheKeys: OptimizerCacheKey = { - lastCommit: await getLastCommit(), workerConfig: config.getWorkerConfig('♻'), - deletedPaths: changes.filter(e => e[1] === 'deleted').map(e => e[0]), - modifiedPaths: {} as Record, + lastCommit, + bootstrap, + deletedPaths, + modifiedTimes: {} as Record, }; - const modified = changes.filter(e => e[1] === 'modified').map(e => e[0]); - const mtimes = await getMtimes(modified); + const mtimes = await getMtimes(modifiedPaths); for (const [path, mtime] of Array.from(mtimes.entries()).sort(ascending(e => e[0]))) { if (typeof mtime === 'number') { - cacheKeys.modifiedPaths[path] = mtime; + cacheKeys.modifiedTimes[path] = mtime; } } diff --git a/packages/kbn-storybook/index.js b/packages/kbn-storybook/index.js index 353a273881343..b595de8ea1c07 100644 --- a/packages/kbn-storybook/index.js +++ b/packages/kbn-storybook/index.js @@ -66,12 +66,17 @@ exports.runStorybookCli = config => { // storybook never completes, so neither will this promise const configDir = join(__dirname, 'storybook_config'); log.debug('Config dir:', configDir); - await storybook({ + + const config = { mode: flags.site ? 'static' : 'dev', port: 9001, configDir, - outputDir: flags.site ? join(ASSET_DIR, name) : undefined, - }); + }; + if (flags.site) { + config.outputDir = join(ASSET_DIR, name); + } + + await storybook(config); // Line is only reached when building the static version if (flags.site) process.exit(); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index fc245ca3fe921..0402a83d3d274 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -17,7 +17,7 @@ "dependencies": { "classnames": "2.2.6", "focus-trap-react": "^3.1.1", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "prop-types": "15.6.0", "react": "^16.12.0", "react-ace": "^5.9.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index acb2b48e12278..e9ad227b235fa 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { - "@elastic/charts": "^17.0.2", + "@elastic/charts": "^17.1.1", "abortcontroller-polyfill": "^1.4.0", "@elastic/eui": "19.0.0", "@kbn/babel-preset": "1.0.0", diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ca2f6789bebee..ba1988b857385 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -932,7 +932,7 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext }> : T; // @public (undocumented) -export interface SavedObject { +export interface SavedObject { attributes: T; // (undocumented) error?: { @@ -975,13 +975,13 @@ export interface SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBatchResponse { +export interface SavedObjectsBatchResponse { // (undocumented) savedObjects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { // (undocumented) attributes: T; // (undocumented) @@ -994,7 +994,7 @@ export interface SavedObjectsBulkCreateOptions { } // @public (undocumented) -export interface SavedObjectsBulkUpdateObject { +export interface SavedObjectsBulkUpdateObject { // (undocumented) attributes: T; // (undocumented) @@ -1017,17 +1017,17 @@ export interface SavedObjectsBulkUpdateOptions { export class SavedObjectsClient { // @internal constructor(http: HttpSetup); - bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; + bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; bulkGet: (objects?: { id: string; type: string; - }[]) => Promise>; - bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; - create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; + }[]) => Promise>; + bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; + create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; - get: (type: string, id: string) => Promise>; - update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; + find: (options: Pick) => Promise>; + get: (type: string, id: string) => Promise>; + update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1069,7 +1069,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { // (undocumented) page: number; // (undocumented) @@ -1176,7 +1176,7 @@ export interface SavedObjectsUpdateOptions { } // @public -export class SimpleSavedObject { +export class SimpleSavedObject { constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); // (undocumented) attributes: T; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index ccb23793a8534..afc77806afb91 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -22,7 +22,6 @@ import { resolve as resolveUrl } from 'url'; import { SavedObject, - SavedObjectAttributes, SavedObjectReference, SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, @@ -61,9 +60,7 @@ export interface SavedObjectsCreateOptions { * * @public */ -export interface SavedObjectsBulkCreateObject< - T extends SavedObjectAttributes = SavedObjectAttributes -> extends SavedObjectsCreateOptions { +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { type: string; attributes: T; } @@ -75,9 +72,7 @@ export interface SavedObjectsBulkCreateOptions { } /** @public */ -export interface SavedObjectsBulkUpdateObject< - T extends SavedObjectAttributes = SavedObjectAttributes -> { +export interface SavedObjectsBulkUpdateObject { type: string; id: string; attributes: T; @@ -99,9 +94,7 @@ export interface SavedObjectsUpdateOptions { } /** @public */ -export interface SavedObjectsBatchResponse< - T extends SavedObjectAttributes = SavedObjectAttributes -> { +export interface SavedObjectsBatchResponse { savedObjects: Array>; } @@ -113,9 +106,7 @@ export interface SavedObjectsBatchResponse< * * @public */ -export interface SavedObjectsFindResponsePublic< - T extends SavedObjectAttributes = SavedObjectAttributes -> extends SavedObjectsBatchResponse { +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { total: number; perPage: number; page: number; @@ -124,7 +115,7 @@ export interface SavedObjectsFindResponsePublic< interface BatchQueueEntry { type: string; id: string; - resolve: (value: SimpleSavedObject | SavedObject) => void; + resolve: (value: SimpleSavedObject | SavedObject) => void; reject: (reason?: any) => void; } @@ -207,7 +198,7 @@ export class SavedObjectsClient { * @param options * @returns */ - public create = ( + public create = ( type: string, attributes: T, options: SavedObjectsCreateOptions = {} @@ -300,7 +291,7 @@ export class SavedObjectsClient { * @property {object} [options.hasReference] - { type, id } * @returns A find result with objects matching the specified search. */ - public find = ( + public find = ( options: SavedObjectsFindOptions ): Promise> => { const path = this.getPath(['_find']); @@ -348,10 +339,7 @@ export class SavedObjectsClient { * @param {string} id * @returns The saved object for the given type and id. */ - public get = ( - type: string, - id: string - ): Promise> => { + public get = (type: string, id: string): Promise> => { if (!type || !id) { return Promise.reject(new Error('requires type and id')); } @@ -402,7 +390,7 @@ export class SavedObjectsClient { * @prop {object} options.migrationVersion - The optional migrationVersion of this document * @returns */ - public update( + public update( type: string, id: string, attributes: T, @@ -434,7 +422,7 @@ export class SavedObjectsClient { * @param {array} objects - [{ type, id, attributes, options: { version, references } }] * @returns The result of the update operation containing both failed and updated saved objects. */ - public bulkUpdate(objects: SavedObjectsBulkUpdateObject[] = []) { + public bulkUpdate(objects: SavedObjectsBulkUpdateObject[] = []) { const path = this.getPath(['_bulk_update']); return this.savedObjectsFetch(path, { @@ -449,9 +437,7 @@ export class SavedObjectsClient { }); } - private createSavedObject( - options: SavedObject - ): SimpleSavedObject { + private createSavedObject(options: SavedObject): SimpleSavedObject { return new SimpleSavedObject(this, options); } diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index 8e464680bcf17..d3ba506b865a4 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -18,7 +18,7 @@ */ import { get, has, set } from 'lodash'; -import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server'; +import { SavedObject as SavedObjectType } from '../../server'; import { SavedObjectsClientContract } from './saved_objects_client'; /** @@ -30,7 +30,7 @@ import { SavedObjectsClientContract } from './saved_objects_client'; * * @public */ -export class SimpleSavedObject { +export class SimpleSavedObject { public attributes: T; // We want to use the same interface this class had in JS public _version?: SavedObjectType['version']; @@ -46,7 +46,7 @@ export class SimpleSavedObject { ) { this.id = id; this.type = type; - this.attributes = attributes || {}; + this.attributes = attributes || ({} as T); this.references = references || []; this._version = version; this.migrationVersion = migrationVersion; diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 50866e5550d8e..b2f4e388b337d 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -31,7 +31,12 @@ export const DEFAULT_API_VERSION = 'master'; export type ElasticsearchConfigType = TypeOf; type SslConfigSchema = ElasticsearchConfigType['ssl']; -const configSchema = schema.object({ +/** + * Validation schema for elasticsearch service config. It can be reused when plugins allow users + * to specify a local elasticsearch config. + * @public + */ +export const configSchema = schema.object({ sniffOnStart: schema.boolean({ defaultValue: false }), sniffInterval: schema.oneOf([schema.duration(), schema.literal(false)], { defaultValue: false, @@ -148,6 +153,10 @@ export const config: ServiceConfigDescriptor = { deprecations, }; +/** + * Wrapper of config schema. + * @public + */ export class ElasticsearchConfig { /** * The interval between health check requests Kibana sends to the Elasticsearch. diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 5d64fadfaa184..cfd72a6fd5e47 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -27,7 +27,7 @@ export { } from './cluster_client'; export { IScopedClusterClient, ScopedClusterClient, Headers } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; -export { config } from './elasticsearch_config'; +export { config, configSchema, ElasticsearchConfig } from './elasticsearch_config'; export { ElasticsearchError, ElasticsearchErrorHelpers } from './errors'; export * from './api_types'; export * from './types'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index cc838ddd1351d..52827b72ee0cc 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -39,7 +39,12 @@ * @packageDocumentation */ -import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch'; +import { + ElasticsearchServiceSetup, + IScopedClusterClient, + configSchema as elasticsearchConfigSchema, +} from './elasticsearch'; + import { HttpServiceSetup } from './http'; import { IScopedRenderingClient } from './rendering'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; @@ -78,6 +83,7 @@ export { Headers, ScopedClusterClient, IScopedClusterClient, + ElasticsearchConfig, ElasticsearchClientConfig, ElasticsearchError, ElasticsearchErrorHelpers, @@ -347,3 +353,14 @@ export { PluginOpaqueId, UuidServiceSetup, }; + +/** + * Config schemas for the platform services. + * + * @alpha + */ +export const config = { + elasticsearch: { + schema: elasticsearchConfigSchema, + }, +}; diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 3fbec7a45148d..ed64e7c4ce0b1 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -7,6 +7,8 @@ - [JSON layout](#json-layout) - [Configuration](#configuration) - [Usage](#usage) +- [Logging config migration](#logging-config-migration) +- [Log record format changes](#log-record-format-changes) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack @@ -321,3 +323,23 @@ Define a custom logger for a specific context. #### logging.filter TBD + +### Log record format changes + +| Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | +| --------------- | ------------------------------------------ | ------------------------------------------ | +| @timestamp | ISO8601 `2012-01-31T23:33:22.011Z` | Absolute `23:33:22.011` | +| context | `parent.child` | `['parent', 'child']` | +| level | `DEBUG` | `['debug']` | +| meta | stringified JSON object `{"to": "v8"}` | N/A | +| pid | can be configured as `%pid` | N/A | + +| Parameter | Platform log record in **json** format | Legacy Platform log record **json** format | +| --------------- | ------------------------------------------ | -------------------------------------------- | +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` | +| context | `context: parent.child` | `tags: ['parent', 'child']` | +| level | `level: DEBUG` | `tags: ['debug']` | +| meta | separate property `"meta": {"to": "v8"}` | merged in log record `{... "to": "v8"}` | +| pid | `pid: 12345` | `pid: 12345` | +| type | N/A | `type: log` | +| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | \ No newline at end of file diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 65ffd4d9a1d57..1a8ede41d0b2c 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -42,10 +42,10 @@ export async function collectSavedObjects({ supportedTypes, }: CollectSavedObjectsOptions) { const errors: SavedObjectsImportError[] = []; - const collectedObjects: SavedObject[] = await createPromiseFromStreams([ + const collectedObjects: Array> = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), - createFilterStream(obj => { + createFilterStream>(obj => { if (supportedTypes.includes(obj.type)) { return true; } diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 725e935f6e21d..5728ce8b7b59f 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -20,11 +20,12 @@ import { SavedObject } from '../types'; import { SavedObjectsImportError } from './types'; export function extractErrors( - savedObjectResults: SavedObject[], - savedObjectsToImport: SavedObject[] + // TODO: define saved object type + savedObjectResults: Array>, + savedObjectsToImport: Array> ) { const errors: SavedObjectsImportError[] = []; - const originalSavedObjectsMap = new Map(); + const originalSavedObjectsMap = new Map>(); for (const savedObject of savedObjectsToImport) { originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); } diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts index 4d9ee59f9df15..f0c033c1d00b4 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/validate_references.ts @@ -77,7 +77,7 @@ export async function getNonExistingReferenceAsKeys( } export async function validateReferences( - savedObjects: SavedObject[], + savedObjects: Array>, savedObjectsClient: SavedObjectsClientContract, namespace?: string ) { diff --git a/src/core/server/saved_objects/management/management.ts b/src/core/server/saved_objects/management/management.ts index 7b5274da91fc8..b7dce2c087c5f 100644 --- a/src/core/server/saved_objects/management/management.ts +++ b/src/core/server/saved_objects/management/management.ts @@ -23,9 +23,9 @@ interface SavedObjectsManagementTypeDefinition { isImportableAndExportable?: boolean; defaultSearchField?: string; icon?: string; - getTitle?: (savedObject: SavedObject) => string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + getTitle?: (savedObject: SavedObject) => string; + getEditUrl?: (savedObject: SavedObject) => string; + getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; } export interface SavedObjectsManagementDefinition { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index aaf6f45c244ec..524c2c8ffae7a 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource { * scenario out of the box. */ interface SavedObjectDoc { - attributes: object; + attributes: unknown; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index b485b8dfe398c..72a7867854b60 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -48,7 +48,6 @@ import { } from '../saved_objects_client'; import { SavedObject, - SavedObjectAttributes, SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, @@ -213,7 +212,7 @@ export class SavedObjectsRepository { * @property {array} [options.references=[]] - [{ name, type, id }] * @returns {promise} - { id, type, version, attributes } */ - public async create( + public async create( type: string, attributes: T, options: SavedObjectsCreateOptions = {} @@ -254,7 +253,7 @@ export class SavedObjectsRepository { body: raw._source, }); - return this._rawToSavedObject({ + return this._rawToSavedObject({ ...raw, ...response, }); @@ -277,7 +276,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} */ - async bulkCreate( + async bulkCreate( objects: Array>, options: SavedObjectsCreateOptions = {} ): Promise> { @@ -464,7 +463,7 @@ export class SavedObjectsRepository { * @property {object} [options.hasReference] - { type, id } * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - async find({ + async find({ search, defaultSearchOperator = 'OR', searchFields, @@ -577,7 +576,7 @@ export class SavedObjectsRepository { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet( + async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { @@ -648,7 +647,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ - async get( + async get( type: string, id: string, options: SavedObjectsBaseOptions = {} @@ -696,7 +695,7 @@ export class SavedObjectsRepository { * @property {array} [options.references] - [{ name, type, id }] * @returns {promise} */ - async update( + async update( type: string, id: string, attributes: Partial, @@ -753,7 +752,7 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} */ - async bulkUpdate( + async bulkUpdate( objects: Array>, options: SavedObjectsBulkUpdateOptions = {} ): Promise> { @@ -972,7 +971,7 @@ export class SavedObjectsRepository { // includes the namespace, and we use this for migrating documents. However, we don't // want the namespace to be returned from the repository, as the repository scopes each // method transparently to the specified namespace. - private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { + private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); return omit(savedObject, 'namespace'); } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index b0b2633646e10..70d69374ba8fe 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -20,7 +20,6 @@ import { ISavedObjectsRepository } from './lib'; import { SavedObject, - SavedObjectAttributes, SavedObjectReference, SavedObjectsMigrationVersion, SavedObjectsBaseOptions, @@ -49,7 +48,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * * @public */ -export interface SavedObjectsBulkCreateObject { +export interface SavedObjectsBulkCreateObject { id?: string; type: string; attributes: T; @@ -62,7 +61,7 @@ export interface SavedObjectsBulkCreateObject +export interface SavedObjectsBulkUpdateObject extends Pick { /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ id: string; @@ -76,7 +75,7 @@ export interface SavedObjectsBulkUpdateObject { +export interface SavedObjectsBulkResponse { saved_objects: Array>; } @@ -88,7 +87,7 @@ export interface SavedObjectsBulkResponse * * @public */ -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { saved_objects: Array>; total: number; per_page: number; @@ -141,7 +140,7 @@ export interface SavedObjectsBulkGetObject { * * @public */ -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { saved_objects: Array>; } @@ -149,7 +148,7 @@ export interface SavedObjectsBulkResponse * * @public */ -export interface SavedObjectsBulkUpdateResponse { +export interface SavedObjectsBulkUpdateResponse { saved_objects: Array>; } @@ -157,7 +156,7 @@ export interface SavedObjectsBulkUpdateResponse +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { attributes: Partial; references: SavedObjectReference[] | undefined; @@ -185,11 +184,7 @@ export class SavedObjectsClient { * @param attributes * @param options */ - async create( - type: string, - attributes: T, - options?: SavedObjectsCreateOptions - ) { + async create(type: string, attributes: T, options?: SavedObjectsCreateOptions) { return await this._repository.create(type, attributes, options); } @@ -199,7 +194,7 @@ export class SavedObjectsClient { * @param objects * @param options */ - async bulkCreate( + async bulkCreate( objects: Array>, options?: SavedObjectsCreateOptions ) { @@ -222,9 +217,7 @@ export class SavedObjectsClient { * * @param options */ - async find( - options: SavedObjectsFindOptions - ): Promise> { + async find(options: SavedObjectsFindOptions): Promise> { return await this._repository.find(options); } @@ -239,7 +232,7 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - async bulkGet( + async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { @@ -253,7 +246,7 @@ export class SavedObjectsClient { * @param id - The ID of the SavedObject to retrieve * @param options */ - async get( + async get( type: string, id: string, options: SavedObjectsBaseOptions = {} @@ -268,7 +261,7 @@ export class SavedObjectsClient { * @param id * @param options */ - async update( + async update( type: string, id: string, attributes: Partial, @@ -282,7 +275,7 @@ export class SavedObjectsClient { * * @param objects */ - async bulkUpdate( + async bulkUpdate( objects: Array>, options?: SavedObjectsBulkUpdateOptions ): Promise> { diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a4fde1765b7d3..9c204784b0aeb 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -33,7 +33,6 @@ export { SavedObjectsImportRetry, } from './import/types'; -import { SavedObjectAttributes } from '../../types'; import { LegacyConfig } from '../legacy'; export { SavedObjectAttributes, @@ -64,7 +63,7 @@ export interface SavedObjectsMigrationVersion { * * @public */ -export interface SavedObject { +export interface SavedObject { /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ id: string; /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ad1907df571fb..f717f30fdb0cf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -503,6 +503,49 @@ export class ClusterClient implements IClusterClient { close(): void; } +// @alpha +export const config: { + elasticsearch: { + schema: import("@kbn/config-schema").ObjectType<{ + sniffOnStart: import("@kbn/config-schema").Type; + sniffInterval: import("@kbn/config-schema").Type; + sniffOnConnectionFault: import("@kbn/config-schema").Type; + hosts: import("@kbn/config-schema").Type; + preserveHost: import("@kbn/config-schema").Type; + username: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + requestHeadersWhitelist: import("@kbn/config-schema").Type; + customHeaders: import("@kbn/config-schema").Type>; + shardTimeout: import("@kbn/config-schema").Type; + requestTimeout: import("@kbn/config-schema").Type; + pingTimeout: import("@kbn/config-schema").Type; + startupTimeout: import("@kbn/config-schema").Type; + logQueries: import("@kbn/config-schema").Type; + ssl: import("@kbn/config-schema").ObjectType<{ + verificationMode: import("@kbn/config-schema").Type<"none" | "full" | "certificate">; + certificateAuthorities: import("@kbn/config-schema").Type; + certificate: import("@kbn/config-schema").Type; + key: import("@kbn/config-schema").Type; + keyPassphrase: import("@kbn/config-schema").Type; + keystore: import("@kbn/config-schema").ObjectType<{ + path: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + }>; + truststore: import("@kbn/config-schema").ObjectType<{ + path: import("@kbn/config-schema").Type; + password: import("@kbn/config-schema").Type; + }>; + alwaysPresentCertificate: import("@kbn/config-schema").Type; + }>; + apiVersion: import("@kbn/config-schema").Type; + healthCheck: import("@kbn/config-schema").ObjectType<{ + delay: import("@kbn/config-schema").Type; + }>; + ignoreVersionMismatch: import("@kbn/config-schema/target/types/types").ConditionalType; + }>; + }; +}; + // @public export type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; @@ -650,8 +693,6 @@ export interface DiscoveredPlugin { readonly requiredPlugins: readonly PluginName[]; } -// Warning: (ae-forgotten-export) The symbol "ElasticsearchConfig" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type ElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; @@ -660,6 +701,31 @@ export type ElasticsearchClientConfig = Pick; }; +// @public +export class ElasticsearchConfig { + constructor(rawConfig: ElasticsearchConfigType); + readonly apiVersion: string; + // Warning: (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts + readonly customHeaders: ElasticsearchConfigType['customHeaders']; + readonly healthCheckDelay: Duration; + readonly hosts: string[]; + readonly ignoreVersionMismatch: boolean; + readonly logQueries: boolean; + readonly password?: string; + readonly pingTimeout: Duration; + readonly requestHeadersWhitelist: string[]; + readonly requestTimeout: Duration; + readonly shardTimeout: Duration; + readonly sniffInterval: false | Duration; + readonly sniffOnConnectionFault: boolean; + readonly sniffOnStart: boolean; + // Warning: (ae-forgotten-export) The symbol "SslConfigSchema" needs to be exported by the entry point index.d.ts + readonly ssl: Pick> & { + certificateAuthorities?: string[]; + }; + readonly username?: string; +} + // @public (undocumented) export interface ElasticsearchError extends Boom { // (undocumented) @@ -1396,7 +1462,7 @@ export interface RouteValidatorOptions { } // @public (undocumented) -export interface SavedObject { +export interface SavedObject { attributes: T; // (undocumented) error?: { @@ -1458,7 +1524,7 @@ export interface SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBulkCreateObject { +export interface SavedObjectsBulkCreateObject { // (undocumented) attributes: T; // (undocumented) @@ -1480,19 +1546,19 @@ export interface SavedObjectsBulkGetObject { } // @public (undocumented) -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { // (undocumented) saved_objects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkResponse { +export interface SavedObjectsBulkResponse { // (undocumented) saved_objects: Array>; } // @public (undocumented) -export interface SavedObjectsBulkUpdateObject extends Pick { +export interface SavedObjectsBulkUpdateObject extends Pick { attributes: Partial; id: string; type: string; @@ -1504,7 +1570,7 @@ export interface SavedObjectsBulkUpdateOptions extends SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsBulkUpdateResponse { +export interface SavedObjectsBulkUpdateResponse { // (undocumented) saved_objects: Array>; } @@ -1513,18 +1579,18 @@ export interface SavedObjectsBulkUpdateResponse(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; - bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; - bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; - create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) errors: typeof SavedObjectsErrorHelpers; - find(options: SavedObjectsFindOptions): Promise>; - get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + find(options: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1706,7 +1772,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { } // @public -export interface SavedObjectsFindResponse { +export interface SavedObjectsFindResponse { // (undocumented) page: number; // (undocumented) @@ -1885,10 +1951,10 @@ export interface SavedObjectsRawDoc { // @public (undocumented) export class SavedObjectsRepository { - bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; - bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; - bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; - create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal @@ -1896,8 +1962,8 @@ export class SavedObjectsRepository { delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; // (undocumented) - find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; - get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ id: string; type: string; @@ -1906,7 +1972,7 @@ export class SavedObjectsRepository { version: string; attributes: any; }>; - update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } // @public @@ -1996,7 +2062,7 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { } // @public (undocumented) -export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { +export interface SavedObjectsUpdateResponse extends Omit, 'attributes' | 'references'> { // (undocumented) attributes: Partial; // (undocumented) @@ -2133,7 +2199,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/plugins/plugins_service.ts:44:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:227:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:228:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 0544a1806e09a..55e32b1e3bb37 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -45,7 +45,10 @@ export async function createOrUpgradeSavedConfig( }); // default to the attributes of the upgradeableConfig if available - const attributes = defaults({ buildNum }, upgradeableConfig ? upgradeableConfig.attributes : {}); + const attributes = defaults( + { buildNum }, + upgradeableConfig ? (upgradeableConfig.attributes as any) : {} + ); try { // create the new SavedConfig diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 3c9c232bff280..a7e55d2b2da65 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -185,7 +185,7 @@ export class UiSettingsClient implements IUiSettingsClient { autoCreateOrUpgradeIfMissing = true, }: ReadOptions = {}): Promise> { try { - const resp = await this.savedObjectsClient.get(this.type, this.id); + const resp = await this.savedObjectsClient.get>(this.type, this.id); return resp.attributes; } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 34ba25f92beb6..d4d2e86e1e96b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -142,6 +142,7 @@ kibana_vars=( xpack.code.security.enableGitCertCheck xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist + xpack.encryptedSavedObjects.encryptionKey xpack.graph.enabled xpack.graph.canEditDrillDownUrls xpack.graph.savePolicy diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 8f7953c408a97..e85e9deff6ddf 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -22,20 +22,8 @@ import { set } from 'lodash'; import { FormattedData } from '../../../../../../plugins/inspector/public'; // @ts-ignore import { createFilter } from './create_filter'; -interface Column { - id: string; - name: string; - aggConfig: any; -} - -interface Row { - [key: string]: any; -} -interface Table { - columns: Column[]; - rows: Row[]; -} +import { TabbedTable } from '../tabify'; /** * @deprecated @@ -52,7 +40,7 @@ interface Table { * inspector. It will only be called when the data view in the inspector is opened. */ export async function buildTabularInspectorData( - table: Table, + table: TabbedTable, queryFilter: { addFilters: (filter: any) => void } ) { const aggConfigs = table.columns.map(column => column.aggConfig); diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts index 0328e87d8b832..cfd4cd7de640b 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -17,8 +17,9 @@ * under the License. */ -import { tabifyGetColumns, AggColumn } from './get_columns'; +import { tabifyGetColumns } from './get_columns'; import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; +import { TabbedAggColumn } from './types'; jest.mock('ui/new_platform'); @@ -140,7 +141,7 @@ describe('get columns', () => { false ); - function checkColumns(column: AggColumn, i: number) { + function checkColumns(column: TabbedAggColumn, i: number) { expect(column).toHaveProperty('aggConfig'); switch (i) { diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts index 54f09f6c6364f..8bffca65b4ae2 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts @@ -19,14 +19,8 @@ import { groupBy } from 'lodash'; import { IAggConfig } from '../aggs'; - -export interface AggColumn { - aggConfig: IAggConfig; - id: string; - name: string; -} - -const getColumn = (agg: IAggConfig, i: number): AggColumn => { +import { TabbedAggColumn } from './types'; +const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => { return { aggConfig: agg, id: `col-${i}-${agg.id}`, @@ -40,14 +34,14 @@ const getColumn = (agg: IAggConfig, i: number): AggColumn => { * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level */ -export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): AggColumn[] { +export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): TabbedAggColumn[] { // pick the columns if (minimalColumns) { return aggs.map((agg, i) => getColumn(agg, i)); } // supposed to be bucket,...metrics,bucket,...metrics - const columns: AggColumn[] = []; + const columns: TabbedAggColumn[] = []; // separate the metrics const grouped = groupBy(aggs, agg => { diff --git a/src/legacy/core_plugins/data/public/search/tabify/index.ts b/src/legacy/core_plugins/data/public/search/tabify/index.ts index be8d64510033c..90ac3f2fb730b 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/index.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -19,3 +19,5 @@ export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; + +export { TabbedTable, TabbedAggRow, TabbedAggColumn } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts index 4c4578e505b71..c910eda024540 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts @@ -19,26 +19,24 @@ import { isEmpty } from 'lodash'; import { IAggConfigs } from '../aggs/agg_configs'; -import { AggColumn, tabifyGetColumns } from './get_columns'; +import { tabifyGetColumns } from './get_columns'; -import { TabbedResponseWriterOptions } from './types'; +import { TabbedResponseWriterOptions, TabbedAggColumn, TabbedAggRow, TabbedTable } from './types'; -interface TabbedAggColumn { +interface BufferColumn { id: string; value: string | number; } -type TabbedAggRow = Record; - /** * Writer class that collects information about an aggregation response and * produces a table, or a series of tables. */ export class TabbedAggResponseWriter { - columns: AggColumn[]; + columns: TabbedAggColumn[]; rows: TabbedAggRow[] = []; - bucketBuffer: TabbedAggColumn[] = []; - metricBuffer: TabbedAggColumn[] = []; + bucketBuffer: BufferColumn[] = []; + metricBuffer: BufferColumn[] = []; private readonly partialRows: boolean; @@ -79,7 +77,7 @@ export class TabbedAggResponseWriter { } } - response() { + response(): TabbedTable { return { columns: this.columns, rows: this.rows, diff --git a/src/legacy/core_plugins/data/public/search/tabify/types.ts b/src/legacy/core_plugins/data/public/search/tabify/types.ts index 3a02a2b64f0c3..964a9d2080e7b 100644 --- a/src/legacy/core_plugins/data/public/search/tabify/types.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/types.ts @@ -18,6 +18,7 @@ */ import { RangeFilterParams } from '../../../../../../plugins/data/public'; +import { IAggConfig } from '../aggs'; /** @internal **/ export interface TabbedRangeFilterParams extends RangeFilterParams { @@ -30,3 +31,19 @@ export interface TabbedResponseWriterOptions { partialRows: boolean; timeRange?: { [key: string]: RangeFilterParams }; } + +/** @public **/ +export interface TabbedAggColumn { + aggConfig: IAggConfig; + id: string; + name: string; +} + +/** @public **/ +export type TabbedAggRow = Record; + +/** @public **/ +export interface TabbedTable { + columns: TabbedAggColumn[]; + rows: TabbedAggRow[]; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts index fe42e07912799..0820ebd371004 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts @@ -23,6 +23,8 @@ export const DashboardConstants = { CREATE_NEW_DASHBOARD_URL: '/dashboard', ADD_EMBEDDABLE_ID: 'addEmbeddableId', ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + DASHBOARDS_ID: 'dashboards', + DASHBOARD_ID: 'dashboard', }; export function createDashboardEditUrl(id: string) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 7d330676e79ed..7d64ee969212f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -83,7 +83,14 @@ export class DashboardPlugin implements Plugin { ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), - defaultSubUrl: '#/dashboards', + defaultSubUrl: `#${DashboardConstants.LANDING_PAGE_PATH}`, + shouldTrackUrlUpdate: pathname => { + const targetAppName = pathname.split('/')[1]; + return ( + targetAppName === DashboardConstants.DASHBOARDS_ID || + targetAppName === DashboardConstants.DASHBOARD_ID + ); + }, storageKey: 'lastUrl:dashboard', navLinkUpdater$: this.appStateUpdater, toastNotifications: core.notifications.toasts, @@ -150,15 +157,15 @@ export class DashboardPlugin implements Plugin { }; kibanaLegacy.registerLegacyApp({ ...app, - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, // only register the updater in once app, otherwise all updates would happen twice updater$: this.appStateUpdater.asObservable(), navLinkId: 'kibana:dashboard', }); - kibanaLegacy.registerLegacyApp({ ...app, id: 'dashboards' }); + kibanaLegacy.registerLegacyApp({ ...app, id: DashboardConstants.DASHBOARDS_ID }); home.featureCatalogue.register({ - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss index 0da28e41579ae..62e7a96ed80cf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/_discover.scss @@ -206,7 +206,7 @@ discover-app { background-color: transparent; } -.dscField--noResults { +.dscFieldName--noResults { color: $euiColorDarkShade; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap index bdb003771619c..23288fc5feb59 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap @@ -1,73 +1,109 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` - - - - t.t.test - - + + + + +
+ + t.t.test + +
+ `; exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = ` - - - + + + + +
- test.test.test - - + + test.test.test + +
+ `; exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = ` - - - + + + + +
- test - - + + test + +
+ `; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 95720bee38df8..54e1c1706a856 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -18,7 +18,9 @@ */ import React from 'react'; import classNames from 'classnames'; -import { FieldIcon } from '../../../../../../../../../plugins/kibana_react/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; import { shortenDottedString } from '../../../../../../../../../plugins/data/common/utils'; import { getFieldTypeName } from './field_type_name'; @@ -35,25 +37,35 @@ interface Props { fieldName?: string; fieldType?: string; useShortDots?: boolean; + fieldIconProps?: Omit; } -export function FieldName({ field, fieldName, fieldType, useShortDots }: Props) { +export function FieldName({ field, fieldName, fieldType, useShortDots, fieldIconProps }: Props) { const type = field ? String(field.type) : String(fieldType); const typeName = getFieldTypeName(type); const name = field ? String(field.name) : String(fieldName); const displayName = useShortDots ? shortenDottedString(name) : name; - const className = classNames({ - 'dscField--noResults': field ? !field.rowCount && !field.scripted : false, - // this is currently not styled, should display an icon - scripted: field ? field.scripted : false, + const noResults = field ? !field.rowCount && !field.scripted : false; + + const className = classNames('dscFieldName', { + 'dscFieldName--noResults': noResults, }); return ( - - - {displayName} - + + + + + + {displayName} + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss index fe13ac2fafa01..b05775c4ee95c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss @@ -10,7 +10,6 @@ .dscFieldName { color: $euiColorDarkShade; - padding-left: $euiSizeS; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx index 96b8cc383888e..b6fd5ee60b8e2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.test.tsx @@ -31,14 +31,14 @@ const indexPattern1 = { attributes: { title: 'test1 title', }, -} as SavedObject; +} as SavedObject; const indexPattern2 = { id: 'test2', attributes: { title: 'test2 title', }, -} as SavedObject; +} as SavedObject; const defaultProps = { indexPatternList: [indexPattern1, indexPattern2], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx index a4e8ee2ca3d8a..cca523ee2c1bd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx @@ -18,6 +18,7 @@ */ import React, { useState } from 'react'; import { SavedObject } from 'kibana/server'; +import { IndexPatternAttributes } from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; @@ -26,11 +27,11 @@ export interface DiscoverIndexPatternProps { /** * list of available index patterns, if length > 1, component offers a "change" link */ - indexPatternList: SavedObject[]; + indexPatternList: Array>; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: SavedObject; + selectedIndexPattern: SavedObject; /** * triggered when user selects a new index pattern */ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx index 7a78e89416361..5b13f6b3655c3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx @@ -87,7 +87,12 @@ export function DocViewTableRow({ )} - + {isCollapsible && ( diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index 7cfc7c4dbc81c..bbb6bf26e5b31 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -24,6 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { indexPatterns, DataPublicPluginStart, + IndexPatternAttributes, } from '../../../../../../../../../../plugins/data/public'; import { SavedObjectsClient, IUiSettingsClient } from '../../../../../../../../../../core/public'; import { MAX_SEARCH_SIZE } from '../../constants'; @@ -96,7 +97,7 @@ export class StepIndexPattern extends Component { - const { savedObjects } = await this.props.savedObjectsClient.find({ + const { savedObjects } = await this.props.savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], perPage: 10000, diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js index 136fe51674bf1..152efe5667f18 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js @@ -30,10 +30,6 @@ import simpleloadPng from './simpleload.png'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; -const colors = { - seedColors, -}; - describe('tag cloud tests', function() { const minValue = 1; const maxValue = 9; @@ -102,6 +98,8 @@ describe('tag cloud tests', function() { let domNode; let tagCloud; + const colorScale = d3.scale.ordinal().range(seedColors); + function setupDOM() { domNode = document.createElement('div'); domNode.style.top = '0'; @@ -132,7 +130,7 @@ describe('tag cloud tests', function() { )}`, function() { beforeEach(async function() { setupDOM(); - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(test.data); tagCloud.setOptions(test.options); await fromNode(cb => tagCloud.once('renderComplete', cb)); @@ -164,7 +162,7 @@ describe('tag cloud tests', function() { //TagCloud takes at least 600ms to complete (due to d3 animation) //renderComplete should only notify at the last one - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -196,7 +194,7 @@ describe('tag cloud tests', function() { describe('should use the latest state before notifying (when modifying options multiple times)', function() { beforeEach(async function() { setupDOM(); - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); tagCloud.setOptions(logScaleTest.options); @@ -223,7 +221,7 @@ describe('tag cloud tests', function() { describe('should use the latest state before notifying (when modifying data multiple times)', function() { beforeEach(async function() { setupDOM(); - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); tagCloud.setData(trimDataTest.data); @@ -253,7 +251,7 @@ describe('tag cloud tests', function() { counter = 0; setupDOM(); return new Promise((resolve, reject) => { - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -299,7 +297,7 @@ describe('tag cloud tests', function() { describe('should show correct data when state-updates are interleaved with resize event', function() { beforeEach(async function() { setupDOM(); - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(logScaleTest.data); tagCloud.setOptions(logScaleTest.options); @@ -337,7 +335,7 @@ describe('tag cloud tests', function() { setupDOM(); domNode.style.width = '1px'; domNode.style.height = '1px'; - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode(cb => tagCloud.once('renderComplete', cb)); @@ -363,7 +361,7 @@ describe('tag cloud tests', function() { domNode.style.width = '1px'; domNode.style.height = '1px'; - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode(cb => tagCloud.once('renderComplete', cb)); @@ -388,7 +386,7 @@ describe('tag cloud tests', function() { describe(`tags should no longer fit after making container smaller`, function() { beforeEach(async function() { setupDOM(); - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode(cb => tagCloud.once('renderComplete', cb)); @@ -420,7 +418,7 @@ describe('tag cloud tests', function() { }); it('should render simple image', async function() { - tagCloud = new TagCloud(domNode, colors); + tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js index f5084fd92cfee..fae7cdf797958 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js @@ -37,7 +37,7 @@ const D3_SCALING_FUNCTIONS = { }; export class TagCloud extends EventEmitter { - constructor(domNode, colors) { + constructor(domNode, colorScale) { super(); //DOM @@ -54,7 +54,6 @@ export class TagCloud extends EventEmitter { this._spiral = 'archimedean'; //layout shape this._timeInterval = 1000; //time allowed for layout algorithm this._padding = 5; - this._seedColors = colors.seedColors; //OPTIONS this._orientation = 'single'; @@ -67,6 +66,7 @@ export class TagCloud extends EventEmitter { this._words = null; //UTIL + this._colorScale = colorScale; this._setTimeoutId = null; this._pendingJob = null; this._layoutIsUpdating = null; @@ -371,8 +371,7 @@ export class TagCloud extends EventEmitter { } getFill(tag) { - const colorScale = d3.scale.ordinal().range(this._seedColors); - return colorScale(tag.text); + return this._colorScale(tag.text); } } diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js index 5528278adf4eb..114643c9a74e0 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js @@ -28,10 +28,12 @@ import { getFormat } from '../legacy_imports'; import { Label } from './label'; import { TagCloud } from './tag_cloud'; import { FeedbackMessage } from './feedback_message'; +import d3 from 'd3'; const MAX_TAG_COUNT = 200; export function createTagCloudVisualization({ colors }) { + const colorScale = d3.scale.ordinal().range(colors.seedColors); return class TagCloudVisualization { constructor(node, vis) { this._containerNode = node; @@ -48,7 +50,7 @@ export function createTagCloudVisualization({ colors }) { this._vis = vis; this._truncated = false; - this._tagCloud = new TagCloud(cloudContainer, colors); + this._tagCloud = new TagCloud(cloudContainer, colorScale); this._tagCloud.on('select', event => { if (!this._visParams.bucket) { return; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts b/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts index 95e01f9c8db5b..ea9532964d6fe 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts @@ -20,7 +20,10 @@ import { get } from 'lodash'; import { getIndexPatterns, getSavedObjectsClient } from './plugin_services'; import { TimelionFunctionArgs } from '../../../../../plugins/timelion/common/types'; -import { indexPatterns as indexPatternsUtils } from '../../../../../plugins/data/public'; +import { + indexPatterns as indexPatternsUtils, + IndexPatternAttributes, +} from '../../../../../plugins/data/public'; export interface Location { min: number; @@ -53,7 +56,7 @@ export function getArgValueSuggestions() { } const indexPatternTitle = get(indexPatternArg, 'value.text'); - const { savedObjects } = await savedObjectsClient.find({ + const { savedObjects } = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], search: `"${indexPatternTitle}"`, @@ -84,7 +87,7 @@ export function getArgValueSuggestions() { es: { async index(partial: string) { const search = partial ? `${partial}*` : '*'; - const resp = await savedObjectsClient.find({ + const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type'], search: `${search}`, diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts b/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts index 25aa77ec73579..cfb2960cfbb7c 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts @@ -18,7 +18,11 @@ */ import { VisSavedObject } from './visualize_embeddable'; -import { indexPatterns, IIndexPattern } from '../../../../../plugins/data/public'; +import { + indexPatterns, + IIndexPattern, + IndexPatternAttributes, +} from '../../../../../plugins/data/public'; import { getUISettings, getSavedObjects } from '../np_ready/public/services'; export async function getIndexPattern( @@ -32,7 +36,7 @@ export async function getIndexPattern( const defaultIndex = getUISettings().get('defaultIndex'); if (savedVis.vis.params.index_pattern) { - const indexPatternObjects = await savedObjectsClient.find({ + const indexPatternObjects = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'fields'], search: `"${savedVis.vis.params.index_pattern}"`, @@ -42,6 +46,9 @@ export async function getIndexPattern( return indexPattern; } - const savedObject = await savedObjectsClient.get('index-pattern', defaultIndex); + const savedObject = await savedObjectsClient.get( + 'index-pattern', + defaultIndex + ); return indexPatterns.getFromSavedObject(savedObject); } diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 4c6c12f825609..72a0ef72b5693 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -24,6 +24,7 @@ import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { EmbeddableVisTriggerContext } from 'src/plugins/embeddable/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -39,8 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - VALUE_CLICK_TRIGGER, - SELECT_RANGE_TRIGGER, + selectRangeTrigger, + valueClickTrigger, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedObject } from '../../../../../plugins/saved_objects/public'; @@ -301,13 +302,14 @@ export class VisualizeEmbeddable extends Embeddable { + const arr = Object.entries(changes).map(([key, value]) => + this.props.uiSettings.set(key, value) + ); + return Promise.all(arr); + }; + render() { const { filteredSettings, query, footerQueryMatched } = this.state; const componentRegistry = this.props.componentRegistry; @@ -205,18 +212,19 @@ export class AdvancedSettingsComponent extends Component<
+
+ + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } > - - -
- - } - title={ -

- Array test setting - -

- } - > - - - - - - - + + + `; exports[`Field for array setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - default_value - , - } - } - /> - - - - - } - title={ -

- Array test setting - -

- } - > - + default_value + , + } + } /> - - } - isInvalid={false} - label="array:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } +> + - - - - - - + + } + label="array:test:setting" + labelType="label" + > + +
+ `; exports[`Field for array setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Array test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Array test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for array setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } > - - -
- - } - title={ -

- Array test setting - -

- } - > - + + +`; + +exports[`Field for array setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Array test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for array setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + default_value + , + } + } /> - - - - default_value - , - } - } - /> - - - - } - title={ -

- Array test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="array:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } +> + + + + + +     + + + } + label="array:test:setting" + labelType="label" + > + + + `; exports[`Field for boolean setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } > - - -
- - } - title={ -

- Boolean test setting - -

- } - > - - - } - onChange={[Function]} - onKeyDown={[Function]} + - - - - - + } + onChange={[Function]} + /> + + `; exports[`Field for boolean setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - true - , - } - } - /> - - - - - } - title={ -

- Boolean test setting - -

- } - > - + true + , + } + } /> - - } - isInvalid={false} - label="boolean:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } +> + - - } - onChange={[Function]} - onKeyDown={[Function]} + + + } + label="boolean:test:setting" + labelType="label" + > + - - - - - + } + onChange={[Function]} + /> +
+ `; exports[`Field for boolean setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Boolean test setting - - } - type="asterisk" - /> -

- } - > - - - } - onChange={[Function]} - onKeyDown={[Function]} + - - - - - + } + onChange={[Function]} + /> + + `; exports[`Field for boolean setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } > - - -
- - } - title={ -

- Boolean test setting - -

+ } - > - - + onChange={[Function]} + /> + + +`; + +exports[`Field for boolean setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + + } + type="asterisk" + /> +

+ } +> + + - - - - - + } + onChange={[Function]} + /> + +

+ Setting is currently not saved. +

+
+ + `; exports[`Field for boolean setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + true + , + } + } /> - - - - true - , - } - } - /> - - - - } - title={ -

- Boolean test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="boolean:test:setting" - labelType="label" - > - + + + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } +> + + + - } - onChange={[Function]} - onKeyDown={[Function]} + +     + + + } + label="boolean:test:setting" + labelType="label" + > + - - - - - + } + onChange={[Function]} + /> +
+ `; exports[`Field for image setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } > - - -
- - } - title={ -

- Image test setting - -

- } - > - - - - - - - + + + `; exports[`Field for image setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - null - , - } - } - /> - - - - - } - title={ -

- Image test setting - -

- } - > - + null + , + } + } /> - - } - isInvalid={false} - label="image:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } +> + - - - - - - + + } + label="image:test:setting" + labelType="label" + > + +
+ `; exports[`Field for image setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Image test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for image setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } > - - -
- - } - title={ -

- Image test setting - -

- } - > - + + +`; + +exports[`Field for image setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Image test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for image setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Image test setting - -

- } - > - - - - - -     - - - - - - - - } - isInvalid={false} - label="image:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } +> + + + + + +     + + + + + + + + } + label="image:test:setting" + labelType="label" + > + + + `; exports[`Field for json setting should render as read only if saving is disabled 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + +
- -
- -
-
- - - - + +
+
+ `; exports[`Field for json setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + } + label="json:test:setting" + labelType="label" + > +
- - - + -
- -
-
- - - - + fullWidth={true} + height="auto" + isReadOnly={true} + maxLines={30} + minLines={6} + mode="json" + onChange={[Function]} + setOptions={ + Object { + "showLineNumbers": false, + "tabSize": 2, + } + } + showGutter={false} + theme="textmate" + value="{\\"hello\\": \\"world\\"}" + width="100%" + /> +
+
+ `; exports[`Field for json setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Json test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Json test setting - - } - type="asterisk" - /> -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for json setting should render default value if there is no user value set 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + + +     + + + } + label="json:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="json:test:setting" - labelType="label" + +
+
+ +`; + +exports[`Field for json setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Json test setting + + } + type="asterisk" + /> +

+ } +> + +
+ +
+ +

-

- -
-
- - - - + Setting is currently not saved. +

+ + + `; exports[`Field for json setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + + +     + + + } + label="json:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="json:test:setting" - labelType="label" - > -
- -
-
- - - - + +
+
+ `; exports[`Field for markdown setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } > - - -
- - } - title={ -

- Markdown test setting - -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for markdown setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Markdown test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } +> + + + + } + label="markdown:test:setting" + labelType="label" + > +
- - - + -
- -
-
- - - - + fullWidth={true} + height="auto" + isReadOnly={true} + maxLines={30} + minLines={6} + mode="markdown" + onChange={[Function]} + setOptions={ + Object { + "showLineNumbers": false, + "tabSize": 2, + } + } + showGutter={false} + theme="textmate" + value="**bold**" + width="100%" + /> +
+
+ `; exports[`Field for markdown setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Markdown test setting - - } - type="asterisk" - /> -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for markdown setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } > - - -
- - } - title={ -

- Markdown test setting - -

- } +
+ +
+ + +`; + +exports[`Field for markdown setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + + } + type="asterisk" + /> +

+ } +> + +
- +
+ +

-

- -
-
- - - - + Setting is currently not saved. +

+ + + `; exports[`Field for markdown setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Markdown test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } +> + + + + + +     + + + } + label="markdown:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="markdown:test:setting" - labelType="label" - > -
- -
-
- - - - + +
+
+ `; exports[`Field for number setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } > - - -
- - } - title={ -

- Number test setting - -

- } - > - - - - - - - + + + `; exports[`Field for number setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - 5 - , - } - } - /> - - - - - } - title={ -

- Number test setting - -

- } - > - + 5 + , + } + } /> - - } - isInvalid={false} - label="number:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + - - - - - - + + } + label="number:test:setting" + labelType="label" + > + +
+ `; exports[`Field for number setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Number test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + +`; + +exports[`Field for number setting should render default value if there is no user value set 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + + + + `; -exports[`Field for number setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Number test setting - -

- } - > - + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for number setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + 5 + , + } + } /> - - - - 5 - , - } - } - /> - - - - } - title={ -

- Number test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="number:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + + + + + +     + + + } + label="number:test:setting" + labelType="label" + > + + + `; exports[`Field for select setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } > - - -
- - } - title={ -

- Select test setting - -

- } - > - - - - - - - + + + `; exports[`Field for select setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - Orange - , - } - } - /> - - - - - } - title={ -

- Select test setting - -

- } - > - + Orange + , + } + } /> - - } - isInvalid={false} - label="select:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } +> + - - - - - - + + } + label="select:test:setting" + labelType="label" + > + +
+ `; exports[`Field for select setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Select test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for select setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } > - - -
- - } - title={ -

- Select test setting - -

- } - > - + + +`; + +exports[`Field for select setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Select test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for select setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + Orange + , + } + } /> - - - - Orange - , - } - } - /> - - - - } - title={ -

- Select test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="select:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } +> + + + + + +     + + + } + label="select:test:setting" + labelType="label" + > + + + `; exports[`Field for string setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + +

+ } > - - -
- - } - title={ -

- String test setting - -

- } - > - - - - - - - + + + `; exports[`Field for string setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - null - , - } - } - /> - - - - - } - title={ -

- String test setting - -

- } - > - + null + , + } + } /> - - } - isInvalid={false} - label="string:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ String test setting + +

+ } +> + - - - - - - + + } + label="string:test:setting" + labelType="label" + > + +
+ `; exports[`Field for string setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- String test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for string setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + +

+ } > - - -
- - } - title={ -

- String test setting - -

- } - > - + + +`; + +exports[`Field for string setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ String test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for string setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- String test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="string:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ String test setting + +

+ } +> + + + + + +     + + + } + label="string:test:setting" + labelType="label" + > + + + `; exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } > - - -
- - } - title={ -

- String test validation setting - -

- } - > - - - - - - - + + + `; exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - foo-default - , - } - } - /> - - - - - } - title={ -

- String test validation setting - -

- } - > - + foo-default + , + } + } /> - - } - isInvalid={false} - label="string:test-validation:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } +> + - - - - - - + + } + label="string:test-validation:setting" + labelType="label" + > + +
+ `; exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- String test validation setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } > - - -
- - } - title={ -

- String test validation setting - -

- } - > - + + +`; + +exports[`Field for stringWithValidation setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + foo-default + , + } + } /> - - - - foo-default - , - } - } - /> - - - - } - title={ -

- String test validation setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="string:test-validation:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } +> + + + + + +     + + + } + label="string:test-validation:setting" + labelType="label" + > + + + `; diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index 81df22ccf6e43..8e41fed685898 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -20,21 +20,14 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { Field } from './field'; - -jest.mock('ui/notify', () => ({ - toastNotifications: { - addDanger: () => {}, - add: jest.fn(), - }, -})); +import { Field, getEditableValue } from './field'; jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); jest.mock('brace/mode/markdown', () => 'brace/mode/markdown'); @@ -45,6 +38,18 @@ const defaults = { category: ['category'], }; +const exampleValues = { + array: ['example_value'], + boolean: false, + image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=', + json: { foo: 'bar2' }, + markdown: 'Hello World', + number: 1, + select: 'banana', + string: 'hello world', + stringWithValidation: 'foo', +}; + const settings: Record = { array: { name: 'array:test:setting', @@ -161,7 +166,7 @@ const settings: Record = { description: 'Description for String test validation setting', type: 'string', validation: { - regex: new RegExp('/^foo'), + regex: new RegExp('^foo'), message: 'must start with "foo"', }, value: undefined, @@ -182,11 +187,22 @@ const userValues = { string: 'foo', stringWithValidation: 'fooUserValue', }; + const invalidUserValues = { stringWithValidation: 'invalidUserValue', }; -const save = jest.fn(() => Promise.resolve(true)); -const clear = jest.fn(() => Promise.resolve(true)); + +const handleChange = jest.fn(); +const clearChange = jest.fn(); + +const getFieldSettingValue = (wrapper: ReactWrapper, name: string, type: string) => { + const field = findTestSubject(wrapper, `advancedSetting-editField-${name}`); + if (type === 'boolean') { + return field.props()['aria-checked']; + } else { + return field.props().value; + } +}; describe('Field', () => { Object.keys(settings).forEach(type => { @@ -197,8 +213,7 @@ describe('Field', () => { const component = shallowWithI18nProvider( { value: userValues[type], isOverridden: true, }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} @@ -232,14 +246,12 @@ describe('Field', () => { const component = shallowWithI18nProvider( ); - expect(component).toMatchSnapshot(); }); @@ -251,8 +263,7 @@ describe('Field', () => { // @ts-ignore value: userValues[type], }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} @@ -269,48 +280,44 @@ describe('Field', () => { ...setting, isCustom: true, }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); - expect(component).toMatchSnapshot(); }); - }); - - if (type === 'select') { - it('should use options for rendering values', () => { - const component = mountWithI18nProvider( + it('should render unsaved value if there are unsaved changes', async () => { + const component = shallowWithI18nProvider( ); - const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); - // @ts-ignore - const labels = select.find('option').map(option => option.prop('value')); - expect(labels).toEqual(['apple', 'orange', 'banana']); + expect(component).toMatchSnapshot(); }); + }); - it('should use optionLabels for rendering labels', () => { + if (type === 'select') { + it('should use options for rendering values and optionsLabels for rendering labels', () => { const component = mountWithI18nProvider( { ); const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); // @ts-ignore + const values = select.find('option').map(option => option.prop('value')); + expect(values).toEqual(['apple', 'orange', 'banana']); + // @ts-ignore const labels = select.find('option').map(option => option.text()); expect(labels).toEqual(['Apple', 'Orange', 'banana']); }); @@ -328,8 +338,8 @@ describe('Field', () => { { const userValue = userValues[type]; (component.instance() as Field).getImageAsBase64 = ({}: Blob) => Promise.resolve(''); - it('should be able to change value from no value and cancel', async () => { - await (component.instance() as Field).onImageChange([userValue]); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); - }); - - it('should be able to change value and save', async () => { - await (component.instance() as Field).onImageChange([userValue]); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: userValue }); + it('should be able to change value and cancel', async () => { + (component.instance() as Field).onImageChange([userValue]); + expect(handleChange).toBeCalled(); await wrapper.setProps({ + unsavedChanges: { + value: userValue, + changeImage: true, + }, setting: { ...(component.instance() as Field).props.setting, value: userValue, }, }); - await (component.instance() as Field).cancelChangeImage(); + expect(clearChange).toBeCalledWith(setting.name); wrapper.update(); }); - it('should be able to change value from existing value and save', async () => { + it('should be able to change value from existing value', async () => { + await wrapper.setProps({ + unsavedChanges: {}, + }); const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-changeImage-${setting.name}`).simulate('click'); - const newUserValue = `${userValue}=`; await (component.instance() as Field).onImageChange([newUserValue]); - const updated2 = wrapper.update(); - findTestSubject(updated2, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: newUserValue }); - await wrapper.setProps({ - setting: { - ...(component.instance() as Field).props.setting, - value: newUserValue, - }, - }); - wrapper.update(); + expect(handleChange).toBeCalled(); }); it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); + expect(handleChange).toBeCalledWith(setting.name, { + value: getEditableValue(setting.type, setting.defVal), + changeImage: true, + }); }); }); } else if (type === 'markdown' || type === 'json') { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); const userValue = userValues[type]; - const fieldUserValue = userValue; - - it('should be able to change value and cancel', async () => { - (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); - }); - it('should be able to change value and save', async () => { - (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: fieldUserValue }); + it('should be able to change value', async () => { + (component.instance() as Field).onCodeEditorChange(userValue as UiSettingsType); + expect(handleChange).toBeCalledWith(setting.name, { value: userValue }); await wrapper.setProps({ setting: { ...(component.instance() as Field).props.setting, @@ -445,19 +417,21 @@ describe('Field', () => { wrapper.update(); }); + it('should be able to reset to default value', async () => { + const updated = wrapper.update(); + findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); + expect(handleChange).toBeCalledWith(setting.name, { + value: getEditableValue(setting.type, setting.defVal), + }); + }); + if (type === 'json') { it('should be able to clear value and have empty object populate', async () => { - (component.instance() as Field).onCodeEditorChange('' as UiSettingsType); + await (component.instance() as Field).onCodeEditorChange('' as UiSettingsType); wrapper.update(); - expect((component.instance() as Field).state.unsavedValue).toEqual('{}'); + expect(handleChange).toBeCalledWith(setting.name, { value: setting.defVal }); }); } - - it('should be able to reset to default value', async () => { - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); - }); }); } else { describe(`for changing ${type} setting`, () => { @@ -470,76 +444,45 @@ describe('Field', () => { // @ts-ignore const invalidUserValue = invalidUserValues[type]; it('should display an error when validation fails', async () => { - (component.instance() as Field).onFieldChange(invalidUserValue); + await (component.instance() as Field).onFieldChange(invalidUserValue); + const expectedUnsavedChanges = { + value: invalidUserValue, + error: (setting.validation as StringValidation).message, + isInvalid: true, + }; + expect(handleChange).toBeCalledWith(setting.name, expectedUnsavedChanges); + wrapper.setProps({ unsavedChanges: expectedUnsavedChanges }); const updated = wrapper.update(); const errorMessage = updated.find('.euiFormErrorText').text(); - expect(errorMessage).toEqual((setting.validation as StringValidation).message); + expect(errorMessage).toEqual(expectedUnsavedChanges.error); }); } - it('should be able to change value and cancel', async () => { - (component.instance() as Field).onFieldChange(fieldUserValue); + it('should be able to change value', async () => { + await (component.instance() as Field).onFieldChange(fieldUserValue); const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); + expect(handleChange).toBeCalledWith(setting.name, { value: fieldUserValue }); + updated.setProps({ unsavedChanges: { value: fieldUserValue } }); + const currentValue = getFieldSettingValue(updated, setting.name, type); + expect(currentValue).toEqual(fieldUserValue); }); - it('should be able to change value and save', async () => { - (component.instance() as Field).onFieldChange(fieldUserValue); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: fieldUserValue }); + it('should be able to reset to default value', async () => { await wrapper.setProps({ - setting: { - ...(component.instance() as Field).props.setting, - value: userValue, - }, + unsavedChanges: {}, + setting: { ...setting, value: fieldUserValue }, }); - wrapper.update(); - }); - - it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); + const expectedEditableValue = getEditableValue(setting.type, setting.defVal); + expect(handleChange).toBeCalledWith(setting.name, { + value: expectedEditableValue, + }); + updated.setProps({ unsavedChanges: { value: expectedEditableValue } }); + const currentValue = getFieldSettingValue(updated, setting.name, type); + expect(currentValue).toEqual(expectedEditableValue); }); }); } }); - - it('should show a reload toast when saving setting requiring a page reload', async () => { - const setting = { - ...settings.string, - requiresPageReload: true, - }; - const toasts = notificationServiceMock.createStartContract().toasts; - const wrapper = mountWithI18nProvider( - - ); - (wrapper.instance() as Field).onFieldChange({ target: { value: 'a new value' } }); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); - expect(save).toHaveBeenCalled(); - await save(); - expect(toasts.add).toHaveBeenCalledWith( - expect.objectContaining({ - title: expect.stringContaining('Please reload the page'), - }) - ); - }); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 7158e3d5e7b3e..d9c3752d1c0a5 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -18,17 +18,16 @@ */ import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import classNames from 'classnames'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; import { EuiBadge, - EuiButton, - EuiButtonEmpty, EuiCode, EuiCodeBlock, + EuiScreenReaderOnly, // @ts-ignore EuiCodeEditor, EuiDescribedFormGroup, @@ -36,23 +35,20 @@ import { EuiFieldText, // @ts-ignore EuiFilePicker, - EuiFlexGroup, - EuiFlexItem, EuiFormRow, EuiIconTip, EuiImage, EuiLink, EuiSpacer, - EuiToolTip, EuiText, EuiSelect, EuiSwitch, EuiSwitchEvent, - keyCodes, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldSetting } from '../../types'; +import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, @@ -64,71 +60,37 @@ import { interface FieldProps { setting: FieldSetting; - save: (name: string, value: string) => Promise; - clear: (name: string) => Promise; + handleChange: (name: string, value: FieldState) => void; enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + clearChange?: (name: string) => void; + unsavedChanges?: FieldState; + loading?: boolean; } -interface FieldState { - unsavedValue: any; - savedValue: any; - loading: boolean; - isInvalid: boolean; - error: string | null; - changeImage: boolean; - isJsonArray: boolean; -} - -export class Field extends PureComponent { - private changeImageForm: EuiFilePicker | undefined; - constructor(props: FieldProps) { - super(props); - const { type, value, defVal } = this.props.setting; - const editableValue = this.getEditableValue(type, value, defVal); - - this.state = { - isInvalid: false, - error: null, - loading: false, - changeImage: false, - savedValue: editableValue, - unsavedValue: editableValue, - isJsonArray: type === 'json' ? Array.isArray(JSON.parse(String(defVal) || '{}')) : false, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: FieldProps) { - const { unsavedValue } = this.state; - const { type, value, defVal } = nextProps.setting; - const editableValue = this.getEditableValue(type, value, defVal); - - this.setState({ - savedValue: editableValue, - unsavedValue: value === null || value === undefined ? editableValue : unsavedValue, - }); +export const getEditableValue = ( + type: UiSettingsType, + value: FieldSetting['value'], + defVal?: FieldSetting['defVal'] +) => { + const val = value === null || value === undefined ? defVal : value; + switch (type) { + case 'array': + return (val as string[]).join(', '); + case 'boolean': + return !!val; + case 'number': + return Number(val); + case 'image': + return val; + default: + return val || ''; } +}; - getEditableValue( - type: UiSettingsType, - value: FieldSetting['value'], - defVal: FieldSetting['defVal'] - ) { - const val = value === null || value === undefined ? defVal : value; - switch (type) { - case 'array': - return (val as string[]).join(', '); - case 'boolean': - return !!val; - case 'number': - return Number(val); - case 'image': - return val; - default: - return val || ''; - } - } +export class Field extends PureComponent { + private changeImageForm: EuiFilePicker | undefined = React.createRef(); getDisplayedDefaultValue( type: UiSettingsType, @@ -150,47 +112,60 @@ export class Field extends PureComponent { } } - setLoading(loading: boolean) { - this.setState({ - loading, - }); - } + handleChange = (unsavedChanges: FieldState) => { + this.props.handleChange(this.props.setting.name, unsavedChanges); + }; - clearError() { - this.setState({ - isInvalid: false, - error: null, - }); + resetField = () => { + const { type, defVal } = this.props.setting; + if (type === 'image') { + this.cancelChangeImage(); + return this.handleChange({ + value: getEditableValue(type, defVal), + changeImage: true, + }); + } + return this.handleChange({ value: getEditableValue(type, defVal) }); + }; + + componentDidUpdate(prevProps: FieldProps) { + if ( + prevProps.setting.type === 'image' && + prevProps.unsavedChanges?.value && + !this.props.unsavedChanges?.value + ) { + this.cancelChangeImage(); + } } onCodeEditorChange = (value: UiSettingsType) => { - const { type } = this.props.setting; - const { isJsonArray } = this.state; + const { defVal, type } = this.props.setting; let newUnsavedValue; - let isInvalid = false; - let error = null; + let errorParams = {}; switch (type) { case 'json': + const isJsonArray = Array.isArray(JSON.parse((defVal as string) || '{}')); newUnsavedValue = value.trim() || (isJsonArray ? '[]' : '{}'); try { JSON.parse(newUnsavedValue); } catch (e) { - isInvalid = true; - error = i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { - defaultMessage: 'Invalid JSON syntax', - }); + errorParams = { + error: i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { + defaultMessage: 'Invalid JSON syntax', + }), + isInvalid: true, + }; } break; default: newUnsavedValue = value; } - this.setState({ - error, - isInvalid, - unsavedValue: newUnsavedValue, + this.handleChange({ + value: newUnsavedValue, + ...errorParams, }); }; @@ -201,58 +176,44 @@ export class Field extends PureComponent { onFieldChangeEvent = (e: React.ChangeEvent) => this.onFieldChange(e.target.value); - onFieldChange = (value: any) => { - const { type, validation } = this.props.setting; - const { unsavedValue } = this.state; - + onFieldChange = (targetValue: any) => { + const { type, validation, value, defVal } = this.props.setting; let newUnsavedValue; switch (type) { case 'boolean': - newUnsavedValue = !unsavedValue; + const { unsavedChanges } = this.props; + const currentValue = unsavedChanges + ? unsavedChanges.value + : getEditableValue(type, value, defVal); + newUnsavedValue = !currentValue; break; case 'number': - newUnsavedValue = Number(value); + newUnsavedValue = Number(targetValue); break; default: - newUnsavedValue = value; + newUnsavedValue = targetValue; } - let isInvalid = false; - let error = null; + let errorParams = {}; - if (validation && (validation as StringValidationRegex).regex) { + if ((validation as StringValidationRegex)?.regex) { if (!(validation as StringValidationRegex).regex!.test(newUnsavedValue.toString())) { - error = (validation as StringValidationRegex).message; - isInvalid = true; + errorParams = { + error: (validation as StringValidationRegex).message, + isInvalid: true, + }; } } - this.setState({ - unsavedValue: newUnsavedValue, - isInvalid, - error, + this.handleChange({ + value: newUnsavedValue, + ...errorParams, }); }; - onFieldKeyDown = ({ keyCode }: { keyCode: number }) => { - if (keyCode === keyCodes.ENTER) { - this.saveEdit(); - } - if (keyCode === keyCodes.ESCAPE) { - this.cancelEdit(); - } - }; - - onFieldEscape = ({ keyCode }: { keyCode: number }) => { - if (keyCode === keyCodes.ESCAPE) { - this.cancelEdit(); - } - }; - onImageChange = async (files: any[]) => { if (!files.length) { - this.clearError(); this.setState({ unsavedValue: null, }); @@ -266,19 +227,24 @@ export class Field extends PureComponent { if (file instanceof File) { base64Image = (await this.getImageAsBase64(file)) as string; } - const isInvalid = !!(maxSize && maxSize.length && base64Image.length > maxSize.length); - this.setState({ - isInvalid, - error: isInvalid - ? i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { - defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', - values: { - maxSizeDescription: maxSize.description, - }, - }) - : null, + + let errorParams = {}; + const isInvalid = !!(maxSize?.length && base64Image.length > maxSize.length); + if (isInvalid) { + errorParams = { + isInvalid, + error: i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { + defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', + values: { + maxSizeDescription: maxSize.description, + }, + }), + }; + } + this.handleChange({ changeImage: true, - unsavedValue: base64Image, + value: base64Image, + ...errorParams, }); } catch (err) { this.props.toasts.addDanger( @@ -305,152 +271,62 @@ export class Field extends PureComponent { } changeImage = () => { - this.setState({ + this.handleChange({ + value: null, changeImage: true, }); }; cancelChangeImage = () => { - const { savedValue } = this.state; - - if (this.changeImageForm) { - this.changeImageForm.fileInput.value = null; - this.changeImageForm.handleChange(); - } - - this.setState({ - changeImage: false, - unsavedValue: savedValue, - }); - }; - - cancelEdit = () => { - const { savedValue } = this.state; - this.clearError(); - this.setState({ - unsavedValue: savedValue, - }); - }; - - showPageReloadToast = () => { - if (this.props.setting.requiresPageReload) { - this.props.toasts.add({ - title: i18n.translate('advancedSettings.field.requiresPageReloadToastDescription', { - defaultMessage: 'Please reload the page for the "{settingName}" setting to take effect.', - values: { - settingName: this.props.setting.displayName || this.props.setting.name, - }, - }), - text: element => { - const content = ( - <> - - - window.location.reload()}> - {i18n.translate('advancedSettings.field.requiresPageReloadToastButtonLabel', { - defaultMessage: 'Reload page', - })} - - - - - ); - ReactDOM.render(content, element); - return () => ReactDOM.unmountComponentAtNode(element); - }, - color: 'success', - }); - } - }; - - saveEdit = async () => { - const { name, defVal, type } = this.props.setting; - const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state; - - if (savedValue === unsavedValue) { - return; - } - - let valueToSave = unsavedValue; - let isSameValue = false; - - switch (type) { - case 'array': - valueToSave = valueToSave.split(',').map((val: string) => val.trim()); - isSameValue = valueToSave.join(',') === (defVal as string[]).join(','); - break; - case 'json': - valueToSave = valueToSave.trim(); - valueToSave = valueToSave || (isJsonArray ? '[]' : '{}'); - default: - isSameValue = valueToSave === defVal; - } - - this.setLoading(true); - try { - if (isSameValue) { - await this.props.clear(name); - } else { - await this.props.save(name, valueToSave); - } - - this.showPageReloadToast(); - - if (changeImage) { - this.cancelChangeImage(); - } - } catch (e) { - this.props.toasts.addDanger( - i18n.translate('advancedSettings.field.saveFieldErrorMessage', { - defaultMessage: 'Unable to save {name}', - values: { name }, - }) - ); + if (this.changeImageForm.current) { + this.changeImageForm.current.fileInput.value = null; + this.changeImageForm.current.handleChange({}); } - this.setLoading(false); - }; - - resetField = async () => { - const { name } = this.props.setting; - this.setLoading(true); - try { - await this.props.clear(name); - this.showPageReloadToast(); - this.cancelChangeImage(); - this.clearError(); - } catch (e) { - this.props.toasts.addDanger( - i18n.translate('advancedSettings.field.resetFieldErrorMessage', { - defaultMessage: 'Unable to reset {name}', - values: { name }, - }) - ); + if (this.props.clearChange) { + this.props.clearChange(this.props.setting.name); } - this.setLoading(false); }; - renderField(setting: FieldSetting) { - const { enableSaving } = this.props; - const { loading, changeImage, unsavedValue } = this.state; - const { name, value, type, options, optionLabels = {}, isOverridden, ariaName } = setting; + renderField(id: string, setting: FieldSetting) { + const { enableSaving, unsavedChanges, loading } = this.props; + const { + name, + value, + type, + options, + optionLabels = {}, + isOverridden, + defVal, + ariaName, + } = setting; + const a11yProps: { [key: string]: string } = unsavedChanges + ? { + 'aria-label': ariaName, + 'aria-describedby': id, + } + : { + 'aria-label': ariaName, + }; + const currentValue = unsavedChanges + ? unsavedChanges.value + : getEditableValue(type, value, defVal); switch (type) { case 'boolean': return ( ) : ( ) } - checked={!!unsavedValue} + checked={!!currentValue} onChange={this.onFieldChangeSwitch} disabled={loading || isOverridden || !enableSaving} - onKeyDown={this.onFieldKeyDown} data-test-subj={`advancedSetting-editField-${name}`} - aria-label={ariaName} + {...a11yProps} /> ); case 'markdown': @@ -458,10 +334,10 @@ export class Field extends PureComponent { return (
{ $blockScrolling: Infinity, }} showGutter={false} + fullWidth />
); case 'image': + const changeImage = unsavedChanges?.changeImage; if (!isDefaultValue(setting) && !changeImage) { - return ( - - ); + return ; } else { return ( { - this.changeImageForm = input; - }} - onKeyDown={this.onFieldEscape} + ref={this.changeImageForm} + fullWidth data-test-subj={`advancedSetting-editField-${name}`} /> ); @@ -501,8 +375,8 @@ export class Field extends PureComponent { case 'select': return ( { return { text: optionLabels.hasOwnProperty(option) ? optionLabels[option] : option, @@ -512,31 +386,31 @@ export class Field extends PureComponent { onChange={this.onFieldChangeEvent} isLoading={loading} disabled={loading || isOverridden || !enableSaving} - onKeyDown={this.onFieldKeyDown} + fullWidth data-test-subj={`advancedSetting-editField-${name}`} /> ); case 'number': return ( ); default: return ( ); @@ -699,8 +573,12 @@ export class Field extends PureComponent { } renderResetToDefaultLink(setting: FieldSetting) { - const { ariaName, name } = setting; - if (isDefaultValue(setting)) { + const { defVal, ariaName, name } = setting; + if ( + defVal === this.props.unsavedChanges?.value || + isDefaultValue(setting) || + this.props.loading + ) { return; } return ( @@ -726,7 +604,7 @@ export class Field extends PureComponent { } renderChangeImageLink(setting: FieldSetting) { - const { changeImage } = this.state; + const changeImage = this.props.unsavedChanges?.changeImage; const { type, value, ariaName, name } = setting; if (type !== 'image' || !value || changeImage) { return; @@ -752,84 +630,49 @@ export class Field extends PureComponent { ); } - renderActions(setting: FieldSetting) { - const { ariaName, name } = setting; - const { loading, isInvalid, changeImage, savedValue, unsavedValue } = this.state; - const isDisabled = loading || setting.isOverridden; - - if (savedValue === unsavedValue && !changeImage) { - return; - } - - return ( - - - - - - - - - (changeImage ? this.cancelChangeImage() : this.cancelEdit())} - disabled={isDisabled} - data-test-subj={`advancedSetting-cancelEditField-${name}`} - > - - - - - - ); - } - render() { - const { setting } = this.props; - const { error, isInvalid } = this.state; + const { setting, unsavedChanges } = this.props; + const error = unsavedChanges?.error; + const isInvalid = unsavedChanges?.isInvalid; + + const className = classNames('mgtAdvancedSettings__field', { + 'mgtAdvancedSettings__field--unsaved': unsavedChanges, + 'mgtAdvancedSettings__field--invalid': isInvalid, + }); + const id = setting.name; return ( - - - - - {this.renderField(setting)} - - - - {this.renderActions(setting)} - + + + <> + {this.renderField(id, setting)} + {unsavedChanges && ( + +

+ {unsavedChanges.error + ? unsavedChanges.error + : i18n.translate('advancedSettings.field.settingIsUnsaved', { + defaultMessage: 'Setting is currently not saved.', + })} +

+
+ )} + +
+
); } } diff --git a/src/plugins/advanced_settings/public/management_app/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts index 5c86519116fe9..d1b9b34515532 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/index.ts +++ b/src/plugins/advanced_settings/public/management_app/components/field/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Field } from './field'; +export { Field, getEditableValue } from './field'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap index 8c471f5f5be9c..bce9cb67537db 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap @@ -1,449 +1,849 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Form should not render no settings message when instructed not to 1`] = ``; +exports[`Form should not render no settings message when instructed not to 1`] = ` + +
+ + + + + +

+ General +

+
+
+
+ + + +
+
+ + + + + + +

+ Dashboard +

+
+
+
+ + +
+
+ + + + + + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } + } + /> + + +
+
+ + +
+
+ +
+
+`; exports[`Form should render no settings message when there are no settings 1`] = ` - - + + + + - - , - } - } + +

+ General +

+
+
+
+ + + +
+
+ -
+ + + + + +

+ Dashboard +

+
+
+
+ + +
+
+ + + + + + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } + } + /> + + +
+
+ + +
+
+ +
`; exports[`Form should render normally 1`] = ` - - - - - + + + + -

- General -

-
-
-
- - +

+ General +

+ + + + + - + -
-
- - - - - - + + + + + + + -

- Dashboard -

- -
-
- - +

+ Dashboard +

+ + + + + -
-
- - - - - - -

- X-pack -

-
- +
+
+ + + + + - - - - - - , - "settingsCount": 9, + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } } - } - /> - - -
-
- - + + + + + + -
-
- + toasts={Object {}} + /> + + + +
`; exports[`Form should render read-only when saving is disabled 1`] = ` - - - - - + + + + -

- General -

-
-
-
- - +

+ General +

+
+
+ + + - + - - - - - - - - + + + + + + + -

- Dashboard -

- -
-
- - +

+ Dashboard +

+ + + + + -
-
- - - - - - -

- X-pack -

-
- +
+
+ + + + + - - - - - - , - "settingsCount": 9, + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } } - } - /> - - -
-
- - + + + + + + -
-
- + toasts={Object {}} + /> + + + +
`; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss new file mode 100644 index 0000000000000..02ebb90221d90 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss @@ -0,0 +1,13 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + +.mgtAdvancedSettingsForm__bottomBar { + margin-left: $euiNavDrawerWidthCollapsed; + z-index: 9; // Puts it inuder the nav drawer when expanded + &--pushForNav { + margin-left: $euiNavDrawerWidthExpanded; + } + @include euiBreakpoint('xs', 's') { + margin-left: 0; + } +} diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss new file mode 100644 index 0000000000000..2ef4ef1d20ce9 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss @@ -0,0 +1 @@ +@import './form'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 468cfbfc70820..0e942665b23a9 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -18,9 +18,14 @@ */ import React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { notificationServiceMock } from '../../../../../../core/public/mocks'; +import { SettingsChanges } from '../../types'; import { Form } from './form'; jest.mock('../field', () => ({ @@ -29,6 +34,25 @@ jest.mock('../field', () => ({ }, })); +beforeAll(() => { + const localStorage: Record = { + 'core.chrome.isLocked': true, + }; + + Object.defineProperty(window, 'localStorage', { + value: { + getItem: (key: string) => { + return localStorage[key] || null; + }, + }, + writable: true, + }); +}); + +afterAll(() => { + delete (window as any).localStorage; +}); + const defaults = { requiresPageReload: false, readOnly: false, @@ -43,50 +67,52 @@ const defaults = { const settings = { dashboard: [ { + ...defaults, name: 'dashboard:test:setting', ariaName: 'dashboard test setting', displayName: 'Dashboard test setting', category: ['dashboard'], - ...defaults, + requiresPageReload: true, }, ], general: [ { + ...defaults, name: 'general:test:date', ariaName: 'general test date', displayName: 'Test date', description: 'bar', category: ['general'], - ...defaults, }, { + ...defaults, name: 'setting:test', ariaName: 'setting test', displayName: 'Test setting', description: 'foo', category: ['general'], - ...defaults, }, ], 'x-pack': [ { + ...defaults, name: 'xpack:test:setting', ariaName: 'xpack test setting', displayName: 'X-Pack test setting', category: ['x-pack'], description: 'bar', - ...defaults, }, ], }; + const categories = ['general', 'dashboard', 'hiddenCategory', 'x-pack']; const categoryCounts = { general: 2, dashboard: 1, 'x-pack': 10, }; -const save = (key: string, value: any) => Promise.resolve(true); -const clear = (key: string) => Promise.resolve(true); +const save = jest.fn((changes: SettingsChanges) => Promise.resolve([true])); + const clearQuery = () => {}; describe('Form', () => { @@ -94,10 +120,10 @@ describe('Form', () => { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { expect(component).toMatchSnapshot(); }); + + it('should hide bottom bar when clicking on the cancel changes button', async () => { + const wrapper = mountWithI18nProvider( + + ); + (wrapper.instance() as Form).setState({ + unsavedChanges: { + 'dashboard:test:setting': { + value: 'changedValue', + }, + }, + }); + const updated = wrapper.update(); + expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(true); + await findTestSubject(updated, `advancedSetting-cancelButton`).simulate('click'); + updated.update(); + expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(false); + }); + + it('should show a reload toast when saving setting requiring a page reload', async () => { + const toasts = notificationServiceMock.createStartContract().toasts; + const wrapper = mountWithI18nProvider( + + ); + (wrapper.instance() as Form).setState({ + unsavedChanges: { + 'dashboard:test:setting': { + value: 'changedValue', + }, + }, + }); + const updated = wrapper.update(); + + findTestSubject(updated, `advancedSetting-saveButton`).simulate('click'); + expect(save).toHaveBeenCalled(); + await save({ 'dashboard:test:setting': 'changedValue' }); + expect(toasts.add).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.stringContaining( + 'One or more settings require you to reload the page to take effect.' + ), + }) + ); + }); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 91d587866836e..ef433dd990d33 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -18,7 +18,7 @@ */ import React, { PureComponent, Fragment } from 'react'; - +import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, @@ -27,30 +27,188 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiTextColor, + EuiBottomBar, + EuiButton, + EuiToolTip, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../kibana_react/public'; import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; -import { Field } from '../field'; -import { FieldSetting } from '../../types'; +import { Field, getEditableValue } from '../field'; +import { FieldSetting, SettingsChanges, FieldState } from '../../types'; type Category = string; +const NAV_IS_LOCKED_KEY = 'core.chrome.isLocked'; interface FormProps { settings: Record; + visibleSettings: Record; categories: Category[]; categoryCounts: Record; clearQuery: () => void; - save: (key: string, value: any) => Promise; - clear: (key: string) => Promise; + save: (changes: SettingsChanges) => Promise; showNoResultsMessage: boolean; enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; } +interface FormState { + unsavedChanges: { + [key: string]: FieldState; + }; + loading: boolean; +} + export class Form extends PureComponent { + state: FormState = { + unsavedChanges: {}, + loading: false, + }; + + setLoading(loading: boolean) { + this.setState({ + loading, + }); + } + + getSettingByKey = (key: string): FieldSetting | undefined => { + return Object.values(this.props.settings) + .flat() + .find(el => el.name === key); + }; + + getCountOfUnsavedChanges = (): number => { + return Object.keys(this.state.unsavedChanges).length; + }; + + getCountOfHiddenUnsavedChanges = (): number => { + const shownSettings = Object.values(this.props.visibleSettings) + .flat() + .map(setting => setting.name); + return Object.keys(this.state.unsavedChanges).filter(key => !shownSettings.includes(key)) + .length; + }; + + areChangesInvalid = (): boolean => { + const { unsavedChanges } = this.state; + return Object.values(unsavedChanges).some(({ isInvalid }) => isInvalid); + }; + + handleChange = (key: string, change: FieldState) => { + const setting = this.getSettingByKey(key); + if (!setting) { + return; + } + const { type, defVal, value } = setting; + const savedValue = getEditableValue(type, value, defVal); + if (change.value === savedValue) { + return this.clearChange(key); + } + this.setState({ + unsavedChanges: { + ...this.state.unsavedChanges, + [key]: change, + }, + }); + }; + + clearChange = (key: string) => { + if (!this.state.unsavedChanges[key]) { + return; + } + const unsavedChanges = { ...this.state.unsavedChanges }; + delete unsavedChanges[key]; + + this.setState({ + unsavedChanges, + }); + }; + + clearAllUnsaved = () => { + this.setState({ unsavedChanges: {} }); + }; + + saveAll = async () => { + this.setLoading(true); + const { unsavedChanges } = this.state; + + if (isEmpty(unsavedChanges)) { + return; + } + const configToSave: SettingsChanges = {}; + let requiresReload = false; + + Object.entries(unsavedChanges).forEach(([name, { value }]) => { + const setting = this.getSettingByKey(name); + if (!setting) { + return; + } + const { defVal, type, requiresPageReload } = setting; + let valueToSave = value; + let equalsToDefault = false; + switch (type) { + case 'array': + valueToSave = valueToSave.split(',').map((val: string) => val.trim()); + equalsToDefault = valueToSave.join(',') === (defVal as string[]).join(','); + break; + case 'json': + const isArray = Array.isArray(JSON.parse((defVal as string) || '{}')); + valueToSave = valueToSave.trim(); + valueToSave = valueToSave || (isArray ? '[]' : '{}'); + default: + equalsToDefault = valueToSave === defVal; + } + if (requiresPageReload) { + requiresReload = true; + } + configToSave[name] = equalsToDefault ? null : valueToSave; + }); + + try { + await this.props.save(configToSave); + this.clearAllUnsaved(); + if (requiresReload) { + this.renderPageReloadToast(); + } + } catch (e) { + this.props.toasts.addDanger( + i18n.translate('advancedSettings.form.saveErrorMessage', { + defaultMessage: 'Unable to save', + }) + ); + } + this.setLoading(false); + }; + + renderPageReloadToast = () => { + this.props.toasts.add({ + title: i18n.translate('advancedSettings.form.requiresPageReloadToastDescription', { + defaultMessage: 'One or more settings require you to reload the page to take effect.', + }), + text: toMountPoint( + <> + + + window.location.reload()}> + {i18n.translate('advancedSettings.form.requiresPageReloadToastButtonLabel', { + defaultMessage: 'Reload page', + })} + + + + + ), + color: 'success', + }); + }; + renderClearQueryLink(totalSettings: number, currentSettings: number) { const { clearQuery } = this.props; @@ -102,8 +260,9 @@ export class Form extends PureComponent { { return null; } + renderCountOfUnsaved = () => { + const unsavedCount = this.getCountOfUnsavedChanges(); + const hiddenUnsavedCount = this.getCountOfHiddenUnsavedChanges(); + return ( + + + + ); + }; + + renderBottomBar = () => { + const areChangesInvalid = this.areChangesInvalid(); + const bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { + 'mgtAdvancedSettingsForm__bottomBar--pushForNav': + localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', + }); + return ( + + + +

{this.renderCountOfUnsaved()}

+
+ + + + + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', + })} + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + + + + + +
+
+ ); + }; + render() { - const { settings, categories, categoryCounts, clearQuery } = this.props; + const { unsavedChanges } = this.state; + const { visibleSettings, categories, categoryCounts, clearQuery } = this.props; const currentCategories: Category[] = []; categories.forEach(category => { - if (settings[category] && settings[category].length) { + if (visibleSettings[category] && visibleSettings[category].length) { currentCategories.push(category); } }); return ( - {currentCategories.length - ? currentCategories.map(category => { - return this.renderCategory(category, settings[category], categoryCounts[category]); - }) - : this.maybeRenderNoSettings(clearQuery)} +
+ {currentCategories.length + ? currentCategories.map(category => { + return this.renderCategory( + category, + visibleSettings[category], + categoryCounts[category] + ); + }) + : this.maybeRenderNoSettings(clearQuery)} +
+ {!isEmpty(unsavedChanges) && this.renderBottomBar()}
); } diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 05bb5e754563d..d44a05ce36f5d 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -47,6 +47,19 @@ export interface FieldSetting { } // until eui searchbar and query are typed + +export interface SettingsChanges { + [key: string]: any; +} + +export interface FieldState { + value?: any; + changeImage?: boolean; + loading?: boolean; + isInvalid?: boolean; + error?: string | null; +} + export interface IQuery { ast: any; // incomplete text: string; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 98cdd20ea4b84..698edbf9cd6a8 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -34,3 +34,15 @@ export interface IIndexPattern { } >; } + +/** + * Use data plugin interface instead + * @deprecated + */ +export interface IndexPatternAttributes { + type: string; + fields: string; + title: string; + typeMeta: string; + timeFieldName?: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index cbd4bfd348797..978f140eb1d26 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -263,6 +263,7 @@ export { IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, + IndexPatternAttributes, } from '../common'; /* diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index 5f95b101302ef..acce5ed57683c 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -50,7 +50,7 @@ export class IndexPatternsService { private async refreshSavedObjectsCache() { this.savedObjectsCache = ( - await this.savedObjectsClient.find({ + await this.savedObjectsClient.find>({ type: 'index-pattern', fields: ['title'], perPage: 10000, diff --git a/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts b/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts index 60b2023f25609..1630a4547b7a1 100644 --- a/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts +++ b/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts @@ -17,17 +17,20 @@ * under the License. */ +import { SavedObject } from 'src/core/public'; import { get } from 'lodash'; -import { IIndexPattern } from '../..'; +import { IIndexPattern, IndexPatternAttributes } from '../..'; -export function getFromSavedObject(savedObject: any): IIndexPattern | undefined { +export function getFromSavedObject( + savedObject: SavedObject +): IIndexPattern | undefined { if (get(savedObject, 'attributes.fields') === undefined) { return; } return { id: savedObject.id, - fields: JSON.parse(savedObject.attributes.fields), + fields: JSON.parse(savedObject.attributes.fields!), title: savedObject.attributes.title, }; } diff --git a/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts b/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts index 6bef11e4fc46c..1e01d2452ce04 100644 --- a/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts +++ b/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts @@ -18,7 +18,7 @@ */ import { isEmpty } from 'lodash'; import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; -import { indexPatterns } from '../..'; +import { indexPatterns, IndexPatternAttributes } from '../..'; export async function fetchIndexPatterns( savedObjectsClient: SavedObjectsClientContract, @@ -30,7 +30,7 @@ export async function fetchIndexPatterns( } const searchString = indexPatternStrings.map(string => `"${string}"`).join(' | '); - const indexPatternsFromSavedObjects = await savedObjectsClient.find({ + const indexPatternsFromSavedObjects = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'fields'], search: searchString, @@ -38,7 +38,7 @@ export async function fetchIndexPatterns( }); const exactMatches = indexPatternsFromSavedObjects.savedObjects.filter(savedObject => { - return indexPatternStrings.includes(savedObject.attributes.title as string); + return indexPatternStrings.includes(savedObject.attributes.title); }); const defaultIndex = uiSettings.get('defaultIndex'); @@ -46,7 +46,10 @@ export async function fetchIndexPatterns( const allMatches = exactMatches.length === indexPatternStrings.length ? exactMatches - : [...exactMatches, await savedObjectsClient.get('index-pattern', defaultIndex)]; + : [ + ...exactMatches, + await savedObjectsClient.get('index-pattern', defaultIndex), + ]; return allMatches.map(indexPatterns.getFromSavedObject); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 40d367138b60d..020c3c4c1192d 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -142,6 +142,7 @@ export { IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, + IndexPatternAttributes, } from '../common'; /** diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 9a364e84092ca..9989345df2796 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -16,56 +16,49 @@ * specific language governing permissions and limitations * under the License. */ - -import { UiActionsSetup, Trigger } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { Filter } from '../../data/public'; import { - CONTEXT_MENU_TRIGGER, - APPLY_FILTER_TRIGGER, + applyFilterTrigger, + contextMenuTrigger, createFilterAction, - PANEL_BADGE_TRIGGER, - SELECT_RANGE_TRIGGER, + panelBadgeTrigger, + selectRangeTrigger, + valueClickTrigger, + EmbeddableVisTriggerContext, + IEmbeddable, + APPLY_FILTER_TRIGGER, VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, + CONTEXT_MENU_TRIGGER, + PANEL_BADGE_TRIGGER, } from './lib'; +declare module '../../ui_actions/public' { + export interface TriggerContextMapping { + [SELECT_RANGE_TRIGGER]: EmbeddableVisTriggerContext; + [VALUE_CLICK_TRIGGER]: EmbeddableVisTriggerContext; + [APPLY_FILTER_TRIGGER]: { + embeddable: IEmbeddable; + filters: Filter[]; + }; + [CONTEXT_MENU_TRIGGER]: object; + [PANEL_BADGE_TRIGGER]: object; + } +} + /** * This method initializes Embeddable plugin with initial set of * triggers and actions. - * - * @param api */ export const bootstrap = (uiActions: UiActionsSetup) => { - const triggerContext: Trigger = { - id: CONTEXT_MENU_TRIGGER, - title: 'Context menu', - description: 'Triggered on top-right corner context-menu select.', - }; - const triggerFilter: Trigger = { - id: APPLY_FILTER_TRIGGER, - title: 'Filter click', - description: 'Triggered when user applies filter to an embeddable.', - }; - const triggerBadge: Trigger = { - id: PANEL_BADGE_TRIGGER, - title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel', - }; - const selectRangeTrigger: Trigger = { - id: SELECT_RANGE_TRIGGER, - title: 'Select range', - description: 'Applies a range filter', - }; - const valueClickTrigger: Trigger = { - id: VALUE_CLICK_TRIGGER, - title: 'Value clicked', - description: 'Value was clicked', - }; + uiActions.registerTrigger(contextMenuTrigger); + uiActions.registerTrigger(applyFilterTrigger); + uiActions.registerTrigger(panelBadgeTrigger); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); + const actionApplyFilter = createFilterAction(); - uiActions.registerTrigger(triggerContext); - uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); - uiActions.registerTrigger(triggerBadge); - uiActions.registerTrigger(selectRangeTrigger); - uiActions.registerTrigger(valueClickTrigger); - // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index b0e14a04a9944..2eafe16442e18 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -23,18 +23,17 @@ import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; export { + Adapters, ADD_PANEL_ACTION_ID, + AddPanelAction, APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, - PANEL_BADGE_TRIGGER, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, - Adapters, - AddPanelAction, - CONTEXT_MENU_TRIGGER, + applyFilterTrigger, Container, ContainerInput, ContainerOutput, + CONTEXT_MENU_TRIGGER, + contextMenuTrigger, EDIT_PANEL_ACTION_ID, EditPanelAction, Embeddable, @@ -42,25 +41,32 @@ export { EmbeddableChildPanelProps, EmbeddableFactory, EmbeddableFactoryNotFoundError, + EmbeddableFactoryRenderer, EmbeddableInput, EmbeddableInstanceConfiguration, EmbeddableOutput, EmbeddablePanel, + EmbeddableRoot, + EmbeddableVisTriggerContext, ErrorEmbeddable, GetEmbeddableFactories, GetEmbeddableFactory, IContainer, IEmbeddable, + isErrorEmbeddable, + openAddPanelFlyout, OutputSpec, + PANEL_BADGE_TRIGGER, + panelBadgeTrigger, PanelNotFoundError, PanelState, PropertySpec, + SELECT_RANGE_TRIGGER, + selectRangeTrigger, + VALUE_CLICK_TRIGGER, + valueClickTrigger, ViewMode, - isErrorEmbeddable, - openAddPanelFlyout, withEmbeddableSubscription, - EmbeddableFactoryRenderer, - EmbeddableRoot, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index 72565b3f527ad..4f981562a49ba 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -17,8 +17,4 @@ * under the License. */ -export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; -export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; -export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; -export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; -export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; +export * from './triggers'; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts new file mode 100644 index 0000000000000..491d9e730eb75 --- /dev/null +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -0,0 +1,65 @@ +/* + * 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 { Trigger } from '../../../../ui_actions/public'; +import { IEmbeddable } from '..'; + +export interface EmbeddableVisTriggerContext { + embeddable: IEmbeddable; + timeFieldName: string; + data: { + e: MouseEvent; + data: unknown; + }; +} + +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', +}; + +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; +export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', +}; + +export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; +export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { + id: CONTEXT_MENU_TRIGGER, + title: 'Context menu', + description: 'Triggered on top-right corner context-menu select.', +}; + +export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { + id: APPLY_FILTER_TRIGGER, + title: 'Filter click', + description: 'Triggered when user applies filter to an embeddable.', +}; + +export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; +export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { + id: PANEL_BADGE_TRIGGER, + title: 'Panel badges', + description: 'Actions appear in title bar when an embeddable loads in a panel', +}; diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 82a7578dd4b89..0b505f117a580 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -17,6 +17,9 @@ * under the License. */ +import { ExpressionValue, ExpressionValueError } from '../expression_types'; +import { ExpressionFunction } from '../../public'; + export type ExpressionAstNode = | ExpressionAstExpression | ExpressionAstFunction @@ -31,6 +34,56 @@ export interface ExpressionAstFunction { type: 'function'; function: string; arguments: Record; + + /** + * Debug information added to each function when expression is executed in *debug mode*. + */ + debug?: ExpressionAstFunctionDebug; +} + +export interface ExpressionAstFunctionDebug { + /** + * True if function successfully returned output, false if function threw. + */ + success: boolean; + + /** + * Reference to the expression function this AST node represents. + */ + fn: ExpressionFunction; + + /** + * Input that expression function received as its first argument. + */ + input: ExpressionValue; + + /** + * Map of resolved arguments expression function received as its second argument. + */ + args: Record; + + /** + * Result returned by the expression function. Including an error result + * if it was returned by the function (not thrown). + */ + output?: ExpressionValue; + + /** + * Error that function threw normalized to `ExpressionValueError`. + */ + error?: ExpressionValueError; + + /** + * Raw error that was thrown by the function, if any. + */ + rawError?: any | Error; + + /** + * Time in milliseconds it took to execute the function. Duration can be + * `undefined` if error happened during argument resolution, because function + * timing starts after the arguments have been resolved. + */ + duration: number | undefined; } export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index b6c1721e33eef..f6ff9efca848b 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -18,20 +18,28 @@ */ import { Execution } from './execution'; -import { parseExpression } from '../ast'; +import { parseExpression, ExpressionAstExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; import { ExpressionFunctionDefinition } from '../../public'; import { ExecutionContract } from './execution_contract'; +beforeAll(() => { + if (typeof performance === 'undefined') { + (global as any).performance = { now: Date.now }; + } +}); + const createExecution = ( expression: string = 'foo bar=123', - context: Record = {} + context: Record = {}, + debug: boolean = false ) => { const executor = createUnitTestExecutor(); const execution = new Execution({ executor, ast: parseExpression(expression), context, + debug, }); return execution; }; @@ -406,4 +414,245 @@ describe('Execution', () => { }); }); }); + + describe('debug mode', () => { + test('can execute expression in debug mode', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 5, + }); + }); + + test('can execute expression with sub-expression in debug mode', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(0); + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 15, + }); + }); + + describe('when functions succeed', () => { + test('sets "success" flag on all functions to true', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(node.debug?.success).toBe(true); + } + }); + + test('stores "fn" reference to the function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(node.debug?.fn.name).toBe('add'); + } + }); + + test('saves duration it took to execute each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(typeof node.debug?.duration).toBe('number'); + expect(node.debug?.duration).toBeLessThan(100); + expect(node.debug?.duration).toBeGreaterThanOrEqual(0); + } + }); + + test('sets duration to 10 milliseconds when function executes 10 milliseconds', async () => { + const execution = createExecution('sleep 10', {}, true); + execution.start(-1); + await execution.result; + + const node = execution.state.get().ast.chain[0]; + expect(typeof node.debug?.duration).toBe('number'); + expect(node.debug?.duration).toBeLessThan(50); + expect(node.debug?.duration).toBeGreaterThanOrEqual(5); + }); + + test('adds .debug field in expression AST on each executed function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(typeof node.debug).toBe('object'); + expect(!!node.debug).toBe(true); + } + }); + + test('stores input of each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.input).toBe(-1); + expect(chain[1].debug!.input).toEqual({ + type: 'num', + value: 0, + }); + expect(chain[2].debug!.input).toEqual({ + type: 'num', + value: 2, + }); + }); + + test('stores output of each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.output).toEqual({ + type: 'num', + value: 0, + }); + expect(chain[1].debug!.output).toEqual({ + type: 'num', + value: 2, + }); + expect(chain[2].debug!.output).toEqual({ + type: 'num', + value: 5, + }); + }); + + test('stores resolved arguments of a function', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.args).toEqual({ + val: 5, + }); + + expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual( + { + name: 'foo', + value: 5, + } + ); + }); + + test('store debug information about sub-expressions', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(0); + await execution.result; + + const { chain } = execution.state.get().ast.chain[0].arguments + .val[0] as ExpressionAstExpression; + + expect(typeof chain[0].debug).toBe('object'); + expect(typeof chain[1].debug).toBe('object'); + expect(!!chain[0].debug).toBe(true); + expect(!!chain[1].debug).toBe(true); + + expect(chain[0].debug!.input).toBe(0); + expect(chain[0].debug!.output).toBe(0); + expect(chain[1].debug!.input).toBe(0); + expect(chain[1].debug!.output).toBe(5); + }); + }); + + describe('when expression throws', () => { + const executor = createUnitTestExecutor(); + executor.registerFunction({ + name: 'throws', + args: {}, + help: '', + fn: () => { + throw new Error('foo'); + }, + }); + + test('stores debug information up until the function that throws', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node1 = execution.state.get().ast.chain[0]; + const node2 = execution.state.get().ast.chain[1]; + const node3 = execution.state.get().ast.chain[2]; + + expect(typeof node1.debug).toBe('object'); + expect(typeof node2.debug).toBe('object'); + expect(typeof node3.debug).toBe('undefined'); + }); + + test('stores error thrown in debug information', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node2 = execution.state.get().ast.chain[1]; + + expect(node2.debug?.error).toMatchObject({ + type: 'error', + error: { + message: '[throws] > foo', + }, + }); + expect(node2.debug?.rawError).toBeInstanceOf(Error); + }); + + test('sets .debug object to expected shape', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node2 = execution.state.get().ast.chain[1]; + + expect(node2.debug).toMatchObject({ + success: false, + fn: expect.any(Object), + input: expect.any(Object), + args: expect.any(Object), + error: expect.any(Object), + rawError: expect.any(Error), + duration: expect.any(Number), + }); + }); + }); + }); }); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 2a272e187cffc..7e7df822724ae 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer } from '../../../kibana_utils/common'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; -import { isExpressionValueError } from '../expression_types/specs/error'; +import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { ExpressionAstExpression, ExpressionAstFunction, @@ -32,7 +32,7 @@ import { parseExpression, } from '../ast'; import { ExecutionContext, DefaultInspectorAdapters } from './types'; -import { getType } from '../expression_types'; +import { getType, ExpressionValue } from '../expression_types'; import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; @@ -44,6 +44,13 @@ export interface ExecutionParams< ast?: ExpressionAstExpression; expression?: string; context?: ExtraContext; + + /** + * Whether to execute expression in *debug mode*. In *debug mode* inputs and + * outputs as well as all resolved arguments and time it took to execute each + * function are saved and are available for introspection. + */ + debug?: boolean; } const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ @@ -190,23 +197,55 @@ export class Execution< } const { function: fnName, arguments: fnArgs } = link; - const fnDef = getByAlias(this.state.get().functions, fnName); + const fn = getByAlias(this.state.get().functions, fnName); - if (!fnDef) { + if (!fn) { return createError({ message: `Function ${fnName} could not be found.` }); } + let args: Record = {}; + let timeStart: number | undefined; + try { - // Resolve arguments before passing to function - // resolveArgs returns an object because the arguments themselves might - // actually have a 'then' function which would be treated as a promise - const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs); - const output = await this.invokeFunction(fnDef, input, resolvedArgs); + // `resolveArgs` returns an object because the arguments themselves might + // actually have a `then` function which would be treated as a `Promise`. + const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); + args = resolvedArgs; + timeStart = this.params.debug ? performance.now() : 0; + const output = await this.invokeFunction(fn, input, resolvedArgs); + + if (this.params.debug) { + const timeEnd: number = performance.now(); + (link as ExpressionAstFunction).debug = { + success: true, + fn, + input, + args: resolvedArgs, + output, + duration: timeEnd - timeStart, + }; + } + if (getType(output) === 'error') return output; input = output; - } catch (e) { - e.message = `[${fnName}] > ${e.message}`; - return createError(e); + } catch (rawError) { + const timeEnd: number = this.params.debug ? performance.now() : 0; + rawError.message = `[${fnName}] > ${rawError.message}`; + const error = createError(rawError) as ExpressionValueError; + + if (this.params.debug) { + (link as ExpressionAstFunction).debug = { + success: false, + fn, + input, + args, + error, + rawError, + duration: timeStart ? timeEnd - timeStart : undefined, + }; + } + + return error; } } @@ -327,7 +366,9 @@ export class Execution< const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { return asts.map((item: ExpressionAstExpression) => { return async (subInput = input) => { - const output = await this.params.executor.interpret(item, subInput); + const output = await this.params.executor.interpret(item, subInput, { + debug: this.params.debug, + }); if (isExpressionValueError(output)) throw output.error; const casted = this.cast(output, argDefs[argName as any].types); return casted; diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index af3662d13de4e..2ecbc5f75a9e8 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -31,6 +31,15 @@ import { ExpressionAstExpression, ExpressionAstNode } from '../ast'; import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; +export interface ExpressionExecOptions { + /** + * Whether to execute expression in *debug mode*. In *debug mode* inputs and + * outputs as well as all resolved arguments and time it took to execute each + * function are saved and are available for introspection. + */ + debug?: boolean; +} + export class TypesRegistry implements IRegistry { constructor(private readonly executor: Executor) {} @@ -145,10 +154,14 @@ export class Executor = Record(ast: ExpressionAstNode, input: T): Promise { + public async interpret( + ast: ExpressionAstNode, + input: T, + options?: ExpressionExecOptions + ): Promise { switch (getType(ast)) { case 'expression': - return await this.interpretExpression(ast as ExpressionAstExpression, input); + return await this.interpretExpression(ast as ExpressionAstExpression, input, options); case 'string': case 'number': case 'null': @@ -161,9 +174,10 @@ export class Executor = Record( ast: string | ExpressionAstExpression, - input: T + input: T, + options?: ExpressionExecOptions ): Promise { - const execution = this.createExecution(ast); + const execution = this.createExecution(ast, undefined, options); execution.start(input); return await execution.result; } @@ -192,7 +206,8 @@ export class Executor = Record( ast: string | ExpressionAstExpression, - context: ExtraContext = {} as ExtraContext + context: ExtraContext = {} as ExtraContext, + { debug }: ExpressionExecOptions = {} as ExpressionExecOptions ): Execution { const params: ExecutionParams = { executor: this, @@ -200,6 +215,7 @@ export class Executor = Record { id: string; name: string; description: string; @@ -83,7 +83,7 @@ export interface SampleDatasetSchema { // Kibana saved objects (index patter, visualizations, dashboard, ...) // Should provide a nice demo of Kibana's functionality with the sample data set - savedObjects: SavedObject[]; + savedObjects: Array>; dataIndices: DataIndexSchema[]; status?: string | undefined; statusMsg?: unknown; diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.ts index aac680211e52e..b6d04c5c0b6a3 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.ts @@ -137,9 +137,9 @@ export class SampleDataRegistry { throw new Error(`Unable to find sample dataset with id: ${sampleDataId}`); } - const dashboard = sampleDataset.savedObjects.find((savedObject: SavedObject) => { + const dashboard = sampleDataset.savedObjects.find(savedObject => { return savedObject.id === dashboardId && savedObject.type === 'dashboard'; - }); + }) as SavedObject<{ panelsJSON: string }>; if (!dashboard) { throw new Error(`Unable to find dashboard with id: ${dashboardId}`); } diff --git a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap index 870dbdc533267..cde6a625ac8e8 100644 --- a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -1,37 +1,159 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FieldIcon renders a blackwhite icon for a string 1`] = ` - `; -exports[`FieldIcon renders a colored icon for a number 1`] = ` - `; -exports[`FieldIcon renders an icon for an unknown type 1`] = ` - +`; + +exports[`FieldIcon renders known field types boolean is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types conflict is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types date is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types geo_point is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types geo_shape is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types ip is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types murmur3 is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types nested is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types number is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types string is rendered 1`] = ` + `; exports[`FieldIcon renders with className if provided 1`] = ` - +`; + +exports[`FieldIcon supports same props as EuiToken 1`] = ` + `; diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx index 90a858e31b4f3..51ff5f603ea37 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx @@ -18,24 +18,44 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldIcon } from './field_icon'; +import { FieldIcon, typeToEuiIconMap } from './field_icon'; -test('FieldIcon renders a blackwhite icon for a string', () => { - const component = shallow(); +const availableTypes = Object.keys(typeToEuiIconMap); + +describe('FieldIcon renders known field types', () => { + availableTypes.forEach(type => { + test(`${type} is rendered`, () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); + +test('FieldIcon renders an icon for an unknown type', () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); -test('FieldIcon renders a colored icon for a number', () => { - const component = shallow(); +test('FieldIcon supports same props as EuiToken', () => { + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); -test('FieldIcon renders an icon for an unknown type', () => { - const component = shallow(); +test('FieldIcon changes fill when scripted is true', () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); test('FieldIcon renders with className if provided', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx index 2e199a7471a64..2da1eba31e254 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx @@ -17,14 +17,10 @@ * under the License. */ import React from 'react'; -import { euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; -import { IconSize } from '@elastic/eui/src/components/icon/icon'; +import classNames from 'classnames'; +import { EuiToken, EuiTokenProps } from '@elastic/eui'; -interface IconMapEntry { - icon: string; - color: string; -} -interface FieldIconProps { +export interface FieldIconProps extends Omit { type: | 'boolean' | 'conflict' @@ -39,51 +35,50 @@ interface FieldIconProps { | string | 'nested'; label?: string; - size?: IconSize; - useColor?: boolean; - className?: string; + scripted?: boolean; } -const colors = euiPaletteColorBlind(); - // defaultIcon => a unknown datatype -const defaultIcon = { icon: 'questionInCircle', color: colors[0] }; +const defaultIcon = { iconType: 'questionInCircle', color: 'gray' }; -export const typeToEuiIconMap: Partial> = { - boolean: { icon: 'invert', color: colors[5] }, +export const typeToEuiIconMap: Partial> = { + boolean: { iconType: 'tokenBoolean' }, // icon for an index pattern mapping conflict in discover - conflict: { icon: 'alert', color: colors[8] }, - date: { icon: 'calendar', color: colors[7] }, - geo_point: { icon: 'globe', color: colors[2] }, - geo_shape: { icon: 'globe', color: colors[2] }, - ip: { icon: 'storage', color: colors[8] }, + conflict: { iconType: 'alert', color: 'euiVisColor9' }, + date: { iconType: 'tokenDate' }, + geo_point: { iconType: 'tokenGeo' }, + geo_shape: { iconType: 'tokenGeo' }, + ip: { iconType: 'tokenIP' }, // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html - murmur3: { icon: 'document', color: colors[1] }, - number: { icon: 'number', color: colors[0] }, - _source: { icon: 'editorCodeBlock', color: colors[3] }, - string: { icon: 'string', color: colors[4] }, - nested: { icon: 'nested', color: colors[2] }, + murmur3: { iconType: 'tokenFile' }, + number: { iconType: 'tokenNumber' }, + _source: { iconType: 'editorCodeBlock', color: 'gray' }, + string: { iconType: 'tokenString' }, + nested: { iconType: 'tokenNested' }, }; /** - * Field icon used across the app + * Field token icon used across the app */ export function FieldIcon({ type, label, size = 's', - useColor = false, - className = undefined, + scripted, + className, + ...rest }: FieldIconProps) { - const euiIcon = typeToEuiIconMap[type] || defaultIcon; + const token = typeToEuiIconMap[type] || defaultIcon; return ( - ); } diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts index 4cf74d991ceb9..701154c06a2ff 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -38,7 +38,7 @@ describe('kbnUrlTracker', () => { let navLinkUpdaterSubject: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>; let toastService: jest.Mocked; - function createTracker() { + function createTracker(shouldTrackUrlUpdate?: (pathname: string) => boolean) { urlTracker = createKbnUrlTracker({ baseUrl: '/app/test', defaultSubUrl: '#/start', @@ -57,6 +57,7 @@ describe('kbnUrlTracker', () => { ], navLinkUpdater$: navLinkUpdaterSubject, toastNotifications: toastService, + shouldTrackUrlUpdate, }); } @@ -82,44 +83,44 @@ describe('kbnUrlTracker', () => { }); test('set nav link to session storage value if defined', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); }); test('set nav link to default if app gets mounted', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); urlTracker.appMounted(); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); }); test('keep nav link to default if path gets changed while app mounted', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); + history.push('/start/deep/path/2'); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); }); test('change nav link to last visited url within app after unmount', () => { createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/3'); }); test('unhash all urls that are recorded while app is mounted', () => { (unhashUrl as jest.Mock).mockImplementation(x => x + '?unhashed'); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); expect(unhashUrl).toHaveBeenCalledTimes(2); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3?unhashed'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/3?unhashed'); }); test('show warning and use hashed url if unhashing does not work', () => { @@ -128,17 +129,17 @@ describe('kbnUrlTracker', () => { }); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); + history.push('/start/deep/path/2'); urlTracker.appUnMounted(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/2'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/2'); expect(toastService.addDanger).toHaveBeenCalledWith('unhash broke'); }); test('change nav link back to default if app gets mounted again', () => { createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); urlTracker.appMounted(); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); @@ -151,11 +152,11 @@ describe('kbnUrlTracker', () => { }); test('update state param without overwriting rest of the url when app is not mounted', () => { - storage.setItem('storageKey', '#/deep/path?extrastate=1'); + storage.setItem('storageKey', '#/start/deep/path?extrastate=1'); createTracker(); state1Subject.next({ key1: 'abc' }); expect(getActiveNavLinkUrl()).toMatchInlineSnapshot( - `"/app/test#/deep/path?extrastate=1&state1=(key1:abc)"` + `"/app/test#/start/deep/path?extrastate=1&state1=(key1:abc)"` ); }); @@ -184,7 +185,45 @@ describe('kbnUrlTracker', () => { test('set url to storage when setActiveUrl was called', () => { createTracker(); - urlTracker.setActiveUrl('/deep/path/4'); - expect(storage.getItem('storageKey')).toEqual('#/deep/path/4'); + urlTracker.setActiveUrl('/start/deep/path/4'); + expect(storage.getItem('storageKey')).toEqual('#/start/deep/path/4'); + }); + + describe('shouldTrackUrlUpdate', () => { + test('change nav link when shouldTrackUrlUpdate is not overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(); + urlTracker.appMounted(); + history.push('/start/path'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/path'); + }); + + test('not change nav link when shouldTrackUrlUpdate is not overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); + }); + + test('change nav link when shouldTrackUrlUpdate is overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(() => true); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/setup/path/2'); + }); + + test('not change nav link when shouldTrackUrlUpdate is overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(() => false); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); + }); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index 2edd135c184ec..b778535a2d428 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -58,6 +58,12 @@ export function createKbnUrlTracker({ toastNotifications, history, storage, + shouldTrackUrlUpdate = pathname => { + const currentAppName = defaultSubUrl.slice(2); // cut hash and slash symbols + const targetAppName = pathname.split('/')[1]; + + return currentAppName === targetAppName; + }, }: { /** * Base url of the current app. This will be used as a prefix for the @@ -82,7 +88,7 @@ export function createKbnUrlTracker({ stateUpdate$: Observable; }>; /** - * Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given ntime. + * Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given time. */ storageKey: string; /** @@ -101,6 +107,13 @@ export function createKbnUrlTracker({ * Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used. */ storage?: Storage; + /** + * Checks if pathname belongs to current app. It's used in history listener to define whether it's necessary to set pathname as active url or not. + * The default implementation compares the app name to the first part of pathname. Consumers can override this function for more complex cases. + * + * @param {string} pathname A location's pathname which comes to history listener + */ + shouldTrackUrlUpdate?: (pathname: string) => boolean; }): KbnUrlTracker { const historyInstance = history || createHashHistory(); const storageInstance = storage || sessionStorage; @@ -148,7 +161,9 @@ export function createKbnUrlTracker({ unsubscribe(); // track current hash when within app unsubscribeURLHistory = historyInstance.listen(location => { - setActiveUrl(location.pathname + location.search); + if (shouldTrackUrlUpdate(location.pathname)) { + setActiveUrl(location.pathname + location.search); + } }); } diff --git a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx index b503392c9827f..81600e9f68634 100644 --- a/src/plugins/saved_objects/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects/public/finder/saved_object_finder.tsx @@ -43,14 +43,13 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; import { - SavedObjectAttributes, SimpleSavedObject, CoreStart, IUiSettingsClient, SavedObjectsStart, -} from '../../../../core/public'; +} from 'src/core/public'; -export interface SavedObjectMetaData { +export interface SavedObjectMetaData { type: string; name: string; getIconForSavedObject(savedObject: SimpleSavedObject): IconType; @@ -59,12 +58,17 @@ export interface SavedObjectMetaData { includeFields?: string[]; } +interface FinderAttributes { + title?: string; + type: string; +} + interface SavedObjectFinderState { items: Array<{ title: string | null; - id: SimpleSavedObject['id']; - type: SimpleSavedObject['type']; - savedObject: SimpleSavedObject; + id: SimpleSavedObject['id']; + type: SimpleSavedObject['type']; + savedObject: SimpleSavedObject; }>; query: string; isFetchingItems: boolean; @@ -78,13 +82,13 @@ interface SavedObjectFinderState { interface BaseSavedObjectFinder { onChoose?: ( - id: SimpleSavedObject['id'], - type: SimpleSavedObject['type'], + id: SimpleSavedObject['id'], + type: SimpleSavedObject['type'], name: string, - savedObject: SimpleSavedObject + savedObject: SimpleSavedObject ) => void; noItemsMessage?: React.ReactNode; - savedObjectMetaData: Array>; + savedObjectMetaData: Array>; showFilter?: boolean; } @@ -128,7 +132,7 @@ class SavedObjectFinderUi extends React.Component< .reduce((allFields, currentFields) => allFields.concat(currentFields), ['title']); const perPage = this.props.uiSettings.get('savedObjects:listingLimit'); - const resp = await this.props.savedObjects.client.find({ + const resp = await this.props.savedObjects.client.find({ type: Object.keys(metaDataMap), fields: [...new Set(fields)], search: query ? `${query}*` : undefined, @@ -163,6 +167,7 @@ class SavedObjectFinderUi extends React.Component< id, type, } = savedObject; + return { title: typeof title === 'string' ? title : '', id, @@ -208,7 +213,7 @@ class SavedObjectFinderUi extends React.Component< ); } - private getSavedObjectMetaDataMap(): Record> { + private getSavedObjectMetaDataMap(): Record { return this.props.savedObjectMetaData.reduce( (map, metaData) => ({ ...map, [metaData.type]: metaData }), {} @@ -470,7 +475,7 @@ class SavedObjectFinderUi extends React.Component< currentSavedObjectMetaData || ({ getIconForSavedObject: () => 'document', - } as Pick, 'getIconForSavedObject'>) + } as Pick, 'getIconForSavedObject'>) ).getIconForSavedObject(item.savedObject); return ( >({ type: this.lowercaseType, search: search ? `${search}*` : undefined, perPage: size, diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.ts b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts index 87e2b7b726e59..2b33489eb27c9 100644 --- a/src/plugins/share/server/routes/lib/short_url_lookup.test.ts +++ b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts @@ -17,9 +17,10 @@ * under the License. */ -import { shortUrlLookupProvider, ShortUrlLookupService } from './short_url_lookup'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; -import { SavedObjectsClient } from '../../../../../core/server'; +import { shortUrlLookupProvider, ShortUrlLookupService, UrlAttributes } from './short_url_lookup'; +import { SavedObjectsClientContract, SavedObject } from 'kibana/server'; + +import { savedObjectsClientMock, loggingServiceMock } from '../../../../../core/server/mocks'; describe('shortUrlLookupProvider', () => { const ID = 'bf00ad16941fc51420f91a93428b27a0'; @@ -31,15 +32,10 @@ describe('shortUrlLookupProvider', () => { let shortUrl: ShortUrlLookupService; beforeEach(() => { - savedObjects = ({ - get: jest.fn(), - create: jest.fn(() => Promise.resolve({ id: ID })), - update: jest.fn(), - errors: SavedObjectsClient.errors, - } as unknown) as jest.Mocked; - + savedObjects = savedObjectsClientMock.create(); + savedObjects.create.mockResolvedValue({ id: ID } as SavedObject); deps = { savedObjects }; - shortUrl = shortUrlLookupProvider({ logger: ({ warn: () => {} } as unknown) as Logger }); + shortUrl = shortUrlLookupProvider({ logger: loggingServiceMock.create().get() }); }); describe('generateUrlId', () => { @@ -55,13 +51,13 @@ describe('shortUrlLookupProvider', () => { const [type, attributes, options] = savedObjects.create.mock.calls[0]; expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual([ + expect(Object.keys(attributes as UrlAttributes).sort()).toEqual([ 'accessCount', 'accessDate', 'createDate', 'url', ]); - expect(attributes.url).toEqual(URL); + expect((attributes as UrlAttributes).url).toEqual(URL); expect(options!.id).toEqual(ID); }); @@ -72,13 +68,13 @@ describe('shortUrlLookupProvider', () => { const [type, attributes] = savedObjects.create.mock.calls[0]; expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual([ + expect(Object.keys(attributes as UrlAttributes).sort()).toEqual([ 'accessCount', 'accessDate', 'createDate', 'url', ]); - expect(attributes.url).toEqual(URL); + expect((attributes as UrlAttributes).url).toEqual(URL); }); it('gracefully handles version conflict', async () => { @@ -119,7 +115,7 @@ describe('shortUrlLookupProvider', () => { expect(type).toEqual(TYPE); expect(id).toEqual(ID); expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); - expect(attributes.accessCount).toEqual(3); + expect((attributes as UrlAttributes).accessCount).toEqual(3); }); }); }); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.ts b/src/plugins/share/server/routes/lib/short_url_lookup.ts index 0d8a9c86621de..65fa33a940fac 100644 --- a/src/plugins/share/server/routes/lib/short_url_lookup.ts +++ b/src/plugins/share/server/routes/lib/short_url_lookup.ts @@ -27,13 +27,20 @@ export interface ShortUrlLookupService { getUrl(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; } +export interface UrlAttributes { + url: string; + accessCount: number; + createDate: number; + accessDate: number; +} + export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrlLookupService { async function updateMetadata( - doc: SavedObject, + doc: SavedObject, { savedObjects }: { savedObjects: SavedObjectsClientContract } ) { try { - await savedObjects.update('url', doc.id, { + await savedObjects.update('url', doc.id, { accessDate: new Date().valueOf(), accessCount: get(doc, 'attributes.accessCount', 0) + 1, }); @@ -53,7 +60,7 @@ export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrl const { isConflictError } = savedObjects.errors; try { - const doc = await savedObjects.create( + const doc = await savedObjects.create( 'url', { url, @@ -75,7 +82,7 @@ export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrl }, async getUrl(id, { savedObjects }) { - const doc = await savedObjects.get('url', id); + const doc = await savedObjects.get('url', id); updateMetadata(doc, { savedObjects }); return doc.attributes.url; diff --git a/src/plugins/telemetry/public/components/telemetry_management_section.tsx b/src/plugins/telemetry/public/components/telemetry_management_section.tsx index 20c8873b13272..bf14c33a48048 100644 --- a/src/plugins/telemetry/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry/public/components/telemetry_management_section.tsx @@ -33,8 +33,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { PRIVACY_STATEMENT_URL } from '../../common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; -// @ts-ignore import { Field } from '../../../advanced_settings/public'; +import { ToastsStart } from '../../../../core/public/'; import { TelemetryService } from '../services/telemetry_service'; const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data']; @@ -44,12 +44,14 @@ interface Props { showAppliesSettingMessage: boolean; enableSaving: boolean; query?: any; + toasts: ToastsStart; } interface State { processing: boolean; showExample: boolean; queryMatches: boolean | null; + enabled: boolean; } export class TelemetryManagementSection extends Component { @@ -57,6 +59,7 @@ export class TelemetryManagementSection extends Component { processing: false, showExample: false, queryMatches: null, + enabled: this.props.telemetryService.getIsOptedIn() || false, }; UNSAFE_componentWillReceiveProps(nextProps: Props) { @@ -79,7 +82,7 @@ export class TelemetryManagementSection extends Component { render() { const { telemetryService } = this.props; - const { showExample, queryMatches } = this.state; + const { showExample, queryMatches, enabled, processing } = this.state; if (!telemetryService.getCanChangeOptInStatus()) { return null; @@ -119,7 +122,7 @@ export class TelemetryManagementSection extends Component { displayName: i18n.translate('telemetry.provideUsageStatisticsTitle', { defaultMessage: 'Provide usage statistics', }), - value: telemetryService.getIsOptedIn(), + value: enabled, description: this.renderDescription(), defVal: true, ariaName: i18n.translate('telemetry.provideUsageStatisticsAriaName', { @@ -127,10 +130,10 @@ export class TelemetryManagementSection extends Component { }), } as any } + loading={processing} dockLinks={null as any} toasts={null as any} - save={this.toggleOptIn} - clear={this.toggleOptIn} + handleChange={this.toggleOptIn} enableSaving={this.props.enableSaving} /> @@ -151,13 +154,13 @@ export class TelemetryManagementSection extends Component {

), @@ -200,20 +203,35 @@ export class TelemetryManagementSection extends Component { ); toggleOptIn = async (): Promise => { - const { telemetryService } = this.props; - const newOptInValue = !telemetryService.getIsOptedIn(); + const { telemetryService, toasts } = this.props; + const newOptInValue = !this.state.enabled; return new Promise((resolve, reject) => { - this.setState({ processing: true }, async () => { - try { - await telemetryService.setOptIn(newOptInValue); - this.setState({ processing: false }); - resolve(true); - } catch (err) { - this.setState({ processing: false }); - reject(err); + this.setState( + { + processing: true, + enabled: newOptInValue, + }, + async () => { + try { + await telemetryService.setOptIn(newOptInValue); + this.setState({ processing: false }); + toasts.addSuccess( + newOptInValue + ? i18n.translate('telemetry.optInSuccessOn', { + defaultMessage: 'Usage data collection turned on.', + }) + : i18n.translate('telemetry.optInSuccessOff', { + defaultMessage: 'Usage data collection turned off.', + }) + ); + resolve(true); + } catch (err) { + this.setState({ processing: false }); + reject(err); + } } - }); + ); }); }; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 83a08b11fa4c2..1ce48d5460b2e 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -29,7 +29,8 @@ export { UiActionsSetup, UiActionsStart } from './plugin'; export { UiActionsServiceParams, UiActionsService } from './service'; export { Action, createAction, IncompatibleActionError } from './actions'; export { buildContextMenuForActions } from './context_menu'; -export { Trigger } from './triggers'; +export { Trigger, TriggerContext } from './triggers'; +export { TriggerContextMapping } from './types'; /** * @deprecated diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index 2bbe106c49a25..8963ba4ddb005 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -68,7 +68,7 @@ describe('UiActionsService', () => { const trigger = service.getTrigger('bar'); - expect(trigger).toEqual({ + expect(trigger).toMatchObject({ description: 'foo', id: 'bar', title: 'baz', @@ -345,8 +345,9 @@ describe('UiActionsService', () => { id: 'bar', title: 'baz', }); + const triggerContract = service.getTrigger('bar'); - expect(triggers.get('bar')).toEqual({ + expect(triggerContract).toMatchObject({ description: 'foo', id: 'bar', title: 'baz', diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index a62d2aa356435..ae409830bbb6e 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -17,10 +17,11 @@ * under the License. */ -import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry } from '../types'; +import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry, TriggerId } from '../types'; import { Action } from '../actions'; -import { Trigger } from '../triggers/trigger'; -import { buildContextMenuForActions, openContextMenu } from '../context_menu'; +import { Trigger, TriggerContext } from '../triggers/trigger'; +import { TriggerInternal } from '../triggers/trigger_internal'; +import { TriggerContract } from '../triggers/trigger_contract'; export interface UiActionsServiceParams { readonly triggers?: TriggerRegistry; @@ -52,18 +53,20 @@ export class UiActionsService { throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`); } - this.triggers.set(trigger.id, trigger); + const triggerInternal = new TriggerInternal(this, trigger); + + this.triggers.set(trigger.id, triggerInternal); this.triggerToActions.set(trigger.id, []); }; - public readonly getTrigger = (id: string) => { - const trigger = this.triggers.get(id); + public readonly getTrigger = (triggerId: T): TriggerContract => { + const trigger = this.triggers.get(triggerId as string); if (!trigger) { - throw new Error(`Trigger [triggerId = ${id}] does not exist.`); + throw new Error(`Trigger [triggerId = ${triggerId}] does not exist.`); } - return trigger; + return trigger.contract; }; public readonly registerAction = (action: Action) => { @@ -128,41 +131,17 @@ export class UiActionsService { ); }; - private async executeSingleAction(action: Action, actionContext: A) { - const href = action.getHref && action.getHref(actionContext); - - if (href) { - window.location.href = href; - return; - } - - await action.execute(actionContext); - } - - private async executeMultipleActions(actions: Action[], actionContext: C) { - const panel = await buildContextMenuForActions({ - actions, - actionContext, - closeMenu: () => session.close(), - }); - const session = openContextMenu([panel]); - } - - public readonly executeTriggerActions = async (triggerId: string, actionContext: C) => { - const actions = await this.getTriggerCompatibleActions!(triggerId, actionContext); - - if (!actions.length) { - throw new Error( - `No compatible actions found to execute for trigger [triggerId = ${triggerId}].` - ); - } - - if (actions.length === 1) { - await this.executeSingleAction(actions[0], actionContext); - return; - } - - await this.executeMultipleActions(actions, actionContext); + /** + * @deprecated + * + * Use `plugins.uiActions.getTrigger(triggerId).exec(params)` instead. + */ + public readonly executeTriggerActions = async ( + triggerId: T, + context: TriggerContext + ) => { + const trigger = this.getTrigger(triggerId); + await trigger.exec(context); }; /** diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts index a34c6eda61ba0..1ae2a19c4001f 100644 --- a/src/plugins/ui_actions/public/triggers/index.ts +++ b/src/plugins/ui_actions/public/triggers/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { Trigger } from './trigger'; +export * from './trigger'; +export * from './trigger_contract'; +export * from './trigger_internal'; diff --git a/src/plugins/ui_actions/public/triggers/trigger.ts b/src/plugins/ui_actions/public/triggers/trigger.ts index ba83f5619e250..2c019b09881d1 100644 --- a/src/plugins/ui_actions/public/triggers/trigger.ts +++ b/src/plugins/ui_actions/public/triggers/trigger.ts @@ -17,8 +17,35 @@ * under the License. */ -export interface Trigger { - id: string; +import { TriggerContextMapping, TriggerId } from '../types'; + +/** + * This is a convenience interface used to register a *trigger*. + * + * `Trigger` specifies a named anchor to which `Action` can be attached. When + * `Trigger` is being *called* it creates a `Context` object and passes it to + * the `execute` method of an `Action`. + * + * More than one action can be attached to a single trigger, in which case when + * trigger is *called* it first displays a context menu for user to pick a + * single action to execute. + */ +export interface Trigger { + /** + * Unique name of the trigger as identified in `ui_actions` plugin trigger + * registry, such as "SELECT_RANGE_TRIGGER" or "VALUE_CLICK_TRIGGER". + */ + id: ID; + + /** + * User friendly name of the trigger. + */ title?: string; + + /** + * A longer user friendly description of the trigger. + */ description?: string; } + +export type TriggerContext = T extends TriggerId ? TriggerContextMapping[T] : never; diff --git a/src/plugins/ui_actions/public/triggers/trigger_contract.ts b/src/plugins/ui_actions/public/triggers/trigger_contract.ts new file mode 100644 index 0000000000000..853b83dccabcc --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/trigger_contract.ts @@ -0,0 +1,56 @@ +/* + * 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 { TriggerContext } from './trigger'; +import { TriggerInternal } from './trigger_internal'; +import { TriggerId } from '../types'; + +/** + * This is a public representation of a trigger that is provided to other plugins. + */ +export class TriggerContract { + /** + * Unique name of the trigger as identified in `ui_actions` plugin trigger + * registry, such as "SELECT_RANGE_TRIGGER" or "VALUE_CLICK_TRIGGER". + */ + public readonly id: T; + + /** + * User friendly name of the trigger. + */ + public readonly title?: string; + + /** + * A longer user friendly description of the trigger. + */ + public readonly description?: string; + + constructor(private readonly internal: TriggerInternal) { + this.id = this.internal.trigger.id; + this.title = this.internal.trigger.title; + this.description = this.internal.trigger.description; + } + + /** + * Use this method to execute action attached to this trigger. + */ + public readonly exec = async (context: TriggerContext) => { + await this.internal.execute(context); + }; +} diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts new file mode 100644 index 0000000000000..efcdc72ecad57 --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -0,0 +1,76 @@ +/* + * 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 { TriggerContext, Trigger } from './trigger'; +import { TriggerContract } from './trigger_contract'; +import { UiActionsService } from '../service'; +import { Action } from '../actions'; +import { buildContextMenuForActions, openContextMenu } from '../context_menu'; +import { TriggerId } from '../types'; + +/** + * Internal representation of a trigger kept for consumption only internally + * within `ui_actions` plugin. + */ +export class TriggerInternal { + public readonly contract = new TriggerContract(this); + + constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {} + + public async execute(context: TriggerContext) { + const triggerId = this.trigger.id; + const actions = await this.service.getTriggerCompatibleActions!(triggerId, context); + + if (!actions.length) { + throw new Error( + `No compatible actions found to execute for trigger [triggerId = ${triggerId}].` + ); + } + + if (actions.length === 1) { + await this.executeSingleAction(actions[0], context); + return; + } + + await this.executeMultipleActions(actions, context); + } + + private async executeSingleAction(action: Action>, context: TriggerContext) { + const href = action.getHref && action.getHref(context); + + if (href) { + window.location.href = href; + return; + } + + await action.execute(context); + } + + private async executeMultipleActions( + actions: Array>>, + context: TriggerContext + ) { + const panel = await buildContextMenuForActions({ + actions, + actionContext: context, + closeMenu: () => session.close(), + }); + const session = openContextMenu([panel]); + } +} diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 9bd6ffdef2af3..8daa893eb4347 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -18,8 +18,14 @@ */ import { Action } from './actions/action'; -import { Trigger } from './triggers/trigger'; +import { TriggerInternal } from './triggers/trigger_internal'; -export type TriggerRegistry = Map; +export type TriggerRegistry = Map>; export type ActionRegistry = Map; export type TriggerToActionsRegistry = Map; + +export type TriggerId = string; + +export interface TriggerContextMapping { + [key: string]: object; +} diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index d7e5064cf7280..ff340c6b0abcd 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -94,7 +94,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider `[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]` ); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -102,14 +102,14 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`); await input.clearValue(); await input.type(propertyValue); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } async toggleAdvancedSettingCheckbox(propertyName: string) { testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 2fa1bf94ad669..32f4fe041423c 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IndexPatternAttributes } from 'src/plugins/data/public'; + import { API_ROUTE } from '../../common/lib/constants'; // @ts-ignore untyped local import { fetch } from '../../common/lib/fetch'; @@ -44,7 +46,7 @@ export const getFields = (index = '_all') => { export const getIndices = () => getSavedObjectsClient() - .find({ + .find({ type: 'index-pattern', fields: ['title'], searchFields: ['title'], @@ -62,7 +64,7 @@ export const getDefaultIndex = () => { return defaultIndexId ? getSavedObjectsClient() - .get('index-pattern', defaultIndexId) + .get('index-pattern', defaultIndexId) .then(defaultIndex => defaultIndex.attributes.title) .catch(err => notify.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })) : Promise.resolve(''); diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index b678a532ec084..f3681f50c56a5 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -12,6 +12,35 @@ import { } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; import { getCoreStart } from '../legacy'; +/* + Remove any top level keys from the workpad which will be rejected by validation +*/ +const validKeys = [ + '@created', + '@timestamp', + 'assets', + 'colors', + 'css', + 'height', + 'id', + 'isWriteable', + 'name', + 'page', + 'pages', + 'width', +]; + +const sanitizeWorkpad = function(workpad) { + const workpadKeys = Object.keys(workpad); + + for (const key of workpadKeys) { + if (!validKeys.includes(key)) { + delete workpad[key]; + } + } + + return workpad; +}; const getApiPath = function() { const basePath = getCoreStart().http.basePath.get(); @@ -29,7 +58,10 @@ const getApiPathAssets = function() { }; export function create(workpad) { - return fetch.post(getApiPath(), { ...workpad, assets: workpad.assets || {} }); + return fetch.post(getApiPath(), { + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + }); } export function get(workpadId) { @@ -41,11 +73,11 @@ export function get(workpadId) { // TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { - return fetch.put(`${getApiPath()}/${id}`, workpad); + return fetch.put(`${getApiPath()}/${id}`, sanitizeWorkpad({ ...workpad })); } export function updateWorkpad(id, workpad) { - return fetch.put(`${getApiPathStructures()}/${id}`, workpad); + return fetch.put(`${getApiPathStructures()}/${id}`, sanitizeWorkpad({ ...workpad })); } export function updateAssets(id, workpadAssets) { diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index d29226c802b06..23e1e1d98aa7f 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -8,9 +8,10 @@ import { mappings } from './mappings'; export const fileUpload = kibana => { return new kibana.Plugin({ - require: ['elasticsearch', 'xpack_main'], + require: ['elasticsearch'], name: 'file_upload', id: 'file_upload', + // TODO: uiExports and savedObjectSchemas to be removed on migration uiExports: { mappings, }, @@ -22,23 +23,14 @@ export const fileUpload = kibana => { init(server) { const coreSetup = server.newPlatform.setup.core; + const coreStart = server.newPlatform.start.core; const { usageCollection } = server.newPlatform.setup.plugins; - const pluginsSetup = { + const pluginsStart = { usageCollection, }; - - // legacy dependencies - const __LEGACY = { - route: server.route.bind(server), - plugins: { - elasticsearch: server.plugins.elasticsearch, - }, - savedObjects: { - getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository, - }, - }; - - new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); + const fileUploadPlugin = new FileUploadPlugin(); + fileUploadPlugin.setup(coreSetup); + fileUploadPlugin.start(coreStart, pluginsStart); }, }); }; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js deleted file mode 100644 index 2e5431bdd6ce2..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +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 { once } from 'lodash'; - -const _callWithInternalUser = once(elasticsearchPlugin => { - const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = elasticsearchPlugin => { - return (...args) => { - return _callWithInternalUser(elasticsearchPlugin)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts deleted file mode 100644 index 04c5013ed8e67..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts +++ /dev/null @@ -1,22 +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 { callWithInternalUserFactory } from './call_with_internal_user_factory'; - -describe('call_with_internal_user_factory', () => { - describe('callWithInternalUserFactory', () => { - it('should use internal user "admin"', () => { - const callWithInternalUser: any = jest.fn(); - const elasticsearchPlugin: any = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); - callWithInternalUserInstance(); - - expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js index a0b0d2d1c7ce3..bef6c369fd9ac 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js @@ -5,14 +5,17 @@ */ import { once } from 'lodash'; +import { getDataClient } from '../kibana_server_services'; -const callWithRequest = once(elasticsearchPlugin => { - const cluster = elasticsearchPlugin.getCluster('data'); - return cluster.callWithRequest; -}); +const callWithRequest = once(() => getDataClient()); -export const callWithRequestFactory = (elasticsearchPlugin, request) => { +export const callWithRequestFactory = request => { return (...args) => { - return callWithRequest(elasticsearchPlugin)(request, ...args); + return ( + callWithRequest() + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js b/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js new file mode 100644 index 0000000000000..104e49015ba80 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +let dataClient; + +export const setElasticsearchClientServices = elasticsearch => { + ({ dataClient } = elasticsearch); +}; +export const getDataClient = () => dataClient; + +let internalRepository; +export const setInternalRepository = createInternalRepository => { + internalRepository = createInternalRepository(); +}; +export const getInternalRepository = () => internalRepository; diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js index 23fb8bda897f0..c448676f813ea 100644 --- a/x-pack/legacy/plugins/file_upload/server/plugin.js +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -5,19 +5,23 @@ */ import { initRoutes } from './routes/file_upload'; +import { setElasticsearchClientServices, setInternalRepository } from './kibana_server_services'; import { registerFileUploadUsageCollector } from './telemetry'; export class FileUploadPlugin { - setup(core, plugins, __LEGACY) { - const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; - const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; - const router = core.http.createRouter(); + constructor() { + this.router = null; + } + + setup(core) { + setElasticsearchClientServices(core.elasticsearch); + this.router = core.http.createRouter(); + } - initRoutes(router, elasticsearchPlugin, getSavedObjectsRepository); + start(core, plugins) { + initRoutes(this.router, core.savedObjects.getSavedObjectsRepository); + setInternalRepository(core.savedObjects.createInternalRepository); - registerFileUploadUsageCollector(plugins.usageCollection, { - elasticsearchPlugin, - getSavedObjectsRepository, - }); + registerFileUploadUsageCollector(plugins.usageCollection); } } diff --git a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js index 1c27c2d7d68e9..acbc907729d95 100644 --- a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js @@ -75,7 +75,7 @@ export const idConditionalValidation = (body, boolHasId) => ) .validate(body); -const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepository) => { +const finishValidationAndProcessReq = () => { return async (con, req, { ok, badRequest }) => { const { query: { id }, @@ -86,7 +86,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos let resp; try { const validIdReqData = idConditionalValidation(body, boolHasId); - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, req); + const callWithRequest = callWithRequestFactory(req); const { importData: importDataFunc } = importDataProvider(callWithRequest); const { index, settings, mappings, ingestPipeline, data } = validIdReqData; @@ -103,7 +103,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos resp = ok({ body: processedReq }); // If no id's been established then this is a new index, update telemetry if (!boolHasId) { - await updateTelemetry({ elasticsearchPlugin, getSavedObjectsRepository }); + await updateTelemetry(); } } else { resp = badRequest(`Error processing request 1: ${processedReq.error.message}`, ['body']); @@ -115,7 +115,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos }; }; -export const initRoutes = (router, esPlugin, getSavedObjectsRepository) => { +export const initRoutes = router => { router.post( { path: `${IMPORT_ROUTE}{id?}`, @@ -125,6 +125,6 @@ export const initRoutes = (router, esPlugin, getSavedObjectsRepository) => { }, options, }, - finishValidationAndProcessReq(esPlugin, getSavedObjectsRepository) + finishValidationAndProcessReq() ); }; diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts index a2b359ae11638..2c2b1183fd5bf 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts @@ -9,19 +9,11 @@ import { getTelemetry, initTelemetry } from './telemetry'; const TELEMETRY_TYPE = 'fileUploadTelemetry'; -export function registerFileUploadUsageCollector( - usageCollection: UsageCollectionSetup, - deps: { - elasticsearchPlugin: any; - getSavedObjectsRepository: any; - } -): void { - const { elasticsearchPlugin, getSavedObjectsRepository } = deps; +export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void { const fileUploadUsageCollector = usageCollection.makeUsageCollector({ type: TELEMETRY_TYPE, isReady: () => true, - fetch: async () => - (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(), + fetch: async () => (await getTelemetry()) || initTelemetry(), }); usageCollection.registerCollector(fileUploadUsageCollector); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts index 1c785d8e7b61c..fadad307c0710 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -6,8 +6,6 @@ import { getTelemetry, updateTelemetry } from './telemetry'; -const elasticsearchPlugin: any = null; -const getSavedObjectsRepository: any = null; const internalRepository = () => ({ get: jest.fn(() => null), create: jest.fn(() => ({ attributes: 'test' })), @@ -25,7 +23,7 @@ describe('file upload plugin telemetry', () => { describe('getTelemetry', () => { it('should get existing telemetry', async () => { const internalRepo = mockInit(); - await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository, internalRepo); + await getTelemetry(internalRepo); expect(internalRepo.update.mock.calls.length).toBe(0); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); @@ -40,11 +38,7 @@ describe('file upload plugin telemetry', () => { }, }); - await updateTelemetry({ - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepo, - }); + await updateTelemetry(internalRepo); expect(internalRepo.update.mock.calls.length).toBe(1); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts index 5ffa735f4c83a..2978dec7aa68d 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts @@ -5,7 +5,8 @@ */ import _ from 'lodash'; -import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; +// @ts-ignore +import { getInternalRepository } from '../kibana_server_services'; export const TELEMETRY_DOC_ID = 'file-upload-telemetry'; @@ -17,27 +18,14 @@ export interface TelemetrySavedObject { attributes: Telemetry; } -export function getInternalRepository( - elasticsearchPlugin: any, - getSavedObjectsRepository: any -): any { - const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); - return getSavedObjectsRepository(callWithInternalUser); -} - export function initTelemetry(): Telemetry { return { filesUploadedTotalCount: 0, }; } -export async function getTelemetry( - elasticsearchPlugin: any, - getSavedObjectsRepository: any, - internalRepo?: object -): Promise { - const internalRepository = - internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); +export async function getTelemetry(internalRepo?: object): Promise { + const internalRepository = internalRepo || getInternalRepository(); let telemetrySavedObject; try { @@ -49,22 +37,9 @@ export async function getTelemetry( return telemetrySavedObject ? telemetrySavedObject.attributes : null; } -export async function updateTelemetry({ - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepo, -}: { - elasticsearchPlugin: any; - getSavedObjectsRepository: any; - internalRepo?: any; -}) { - const internalRepository = - internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); - let telemetry = await getTelemetry( - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepository - ); +export async function updateTelemetry(internalRepo?: any) { + const internalRepository = internalRepo || getInternalRepository(); + let telemetry = await getTelemetry(internalRepository); // Create if doesn't exist if (!telemetry || _.isEmpty(telemetry)) { const newTelemetrySavedObject = await internalRepository.create( diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx index 6152f33350917..f2a4c28afcdae 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx @@ -237,10 +237,18 @@ export function FieldEditor({ renderOption={(option, searchValue, contentClassName) => { const { type, label } = option; return ( - - {' '} - {label} - + + + + + + {label} + + ); }} compressed diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx deleted file mode 100644 index 0c099135f631d..0000000000000 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx +++ /dev/null @@ -1,37 +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 { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; - -function stringToNum(s: string) { - return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1); -} - -function getIconForDataType(dataType: string) { - const icons: Partial>> = { - boolean: 'invert', - date: 'calendar', - geo_point: 'globe', - ip: 'storage', - }; - return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'document'; -} - -export function getColorForDataType(type: string) { - const iconType = getIconForDataType(type); - const colors = euiPaletteColorBlind(); - const colorIndex = stringToNum(iconType) % colors.length; - return colors[colorIndex]; -} - -export type UnwrapArray = T extends Array ? P : T; - -export function FieldIcon({ type }: { type: string }) { - const iconType = getIconForDataType(type); - - return ; -} diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx index b38e3f8430980..30f1fcffd4f67 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx @@ -122,7 +122,7 @@ function toOptions( .filter(field => isExplorable(field) || field.selected) .map(field => ({ label: field.name, - prepend: , + prepend: , checked: field.selected ? 'on' : undefined, })) ); diff --git a/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts index 184fbc01eb96f..d9bb119006e78 100644 --- a/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts +++ b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts @@ -52,7 +52,7 @@ export function createSavedWorkspacesLoader( }, find: (searchString: string, size: number = 100) => { return savedObjectsClient - .find({ + .find>({ type: SavedWorkspace.type, search: searchString ? `${searchString}*` : undefined, perPage: size, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap index 5593a1af00d70..8bbe49b2e0d7f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap @@ -1,10 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`LensFieldIcon accepts FieldIcon props 1`] = ` + +`; + exports[`LensFieldIcon renders properly 1`] = ` `; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss index ed39beeb7d088..77d4b41a0413c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss @@ -52,3 +52,16 @@ @include euiFormControlLayoutPadding(1, 'right'); @include euiFormControlLayoutPadding(1, 'left'); } + +.lnsInnerIndexPatternDataPanel__filterType { + padding: $euiSizeS; +} + +.lnsInnerIndexPatternDataPanel__filterTypeInner { + display: flex; + align-items: center; + + .lnsFieldListPanel__fieldIcon { + margin-right: $euiSizeS; + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss index 54f9a3787466d..89f6bbf908419 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -14,7 +14,7 @@ } .lnsFieldItem--missing { - background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); + background: lightOrDarkTheme(transparentize($euiColorMediumShade, 0.9), $euiColorEmptyShade); color: $euiColorDarkShade; } @@ -24,10 +24,10 @@ display: flex; align-items: flex-start; transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance, - background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation + background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation .lnsFieldItem__name { - margin-left: $euiSizeXS; + margin-left: $euiSizeS; flex-grow: 1; } @@ -37,7 +37,8 @@ } .lnsFieldListPanel__fieldIcon { - margin-top: 2px; + margin-top: $euiSizeXS / 2; + margin-right: $euiSizeXS / 2; } .lnsFieldItem__infoIcon { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 3231ab7d7ff12..69982aed78b40 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -384,6 +384,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ data-test-subj="lnsIndexPatternTypeFilterOptions" items={(availableFieldTypes as DataType[]).map(type => ( - {fieldTypeNames[type]} + + {fieldTypeNames[type]} + ))} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 46d7233ba9587..77435fcdf3eed 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -169,6 +169,7 @@ export function FieldSelect({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx deleted file mode 100644 index 6b12bb5feef1b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx +++ /dev/null @@ -1,55 +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. - */ - -/* - * 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 { shallow } from 'enzyme'; -import React from 'react'; -import { FieldIcon } from './field_icon'; - -describe('FieldIcon', () => { - it('should render icons', () => { - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx deleted file mode 100644 index 796f200bffd97..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx +++ /dev/null @@ -1,43 +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 { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; -import classNames from 'classnames'; -import { DataType } from '../types'; - -function stringToNum(s: string) { - return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1); -} - -function getIconForDataType(dataType: string) { - const icons: Partial>> = { - boolean: 'invert', - date: 'calendar', - ip: 'ip', - }; - return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'empty'; -} - -export function getColorForDataType(type: string) { - const iconType = getIconForDataType(type); - const colors = euiPaletteColorBlind(); - const colorIndex = stringToNum(iconType) % colors.length; - return colors[colorIndex]; -} - -export type UnwrapArray = T extends Array ? P : T; - -export function FieldIcon({ type }: { type: DataType }) { - const iconType = getIconForDataType(type); - - const classes = classNames( - 'lnsFieldListPanel__fieldIcon', - `lnsFieldListPanel__fieldIcon--${type}` - ); - - return ; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx index 0271d2ca021c5..94d644e6590e1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -46,7 +46,7 @@ import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../../../../plugins/lens/common'; import { IndexPattern, IndexPatternField } from './types'; -import { getColorForDataType, LensFieldIcon } from './lens_field_icon'; +import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { @@ -294,11 +294,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { ); } - const euiButtonColor = - field.type === 'string' ? 'accent' : field.type === 'number' ? 'secondary' : 'primary'; - const euiTextColor = - field.type === 'string' ? 'accent' : field.type === 'number' ? 'secondary' : 'default'; - const fromDate = DateMath.parse(dateRange.fromDate); const toDate = DateMath.parse(dateRange.toDate); @@ -391,8 +386,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { defaultMessage: 'Count', }); - const expectedColor = getColorForDataType(field.type); - const seriesColors = expectedColor ? [expectedColor] : undefined; if (field.type === 'date') { return wrapInPopover( @@ -429,7 +422,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { yAccessors={['count']} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} - customSeriesColors={seriesColors} timeZone="local" /> @@ -453,7 +445,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { yAccessors={['count']} xScaleType={ScaleType.Linear} yScaleType={ScaleType.Linear} - customSeriesColors={seriesColors} /> ); @@ -486,7 +477,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { )} - + {Math.round((topValue.count / props.sampledValues!) * 100)}% @@ -497,7 +488,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { value={topValue.count / props.sampledValues!} max={1} size="s" - color={euiButtonColor} + color="accent" />

); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx index 961e22380bdca..317ce8f032f94 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx @@ -11,19 +11,14 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import { LensFieldIcon, getColorForDataType } from './lens_field_icon'; +import { LensFieldIcon } from './lens_field_icon'; test('LensFieldIcon renders properly', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); -test('LensFieldIcon getColorForDataType for a valid type', () => { - const color = getColorForDataType('date'); - expect(color).toEqual('#DA8B45'); -}); - -test('LensFieldIcon getColorForDataType for an invalid type', () => { - const color = getColorForDataType('invalid'); - expect(color).toEqual('#54B399'); +test('LensFieldIcon accepts FieldIcon props', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx index 2e6a5fcd8115f..06eda73748cef 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx @@ -5,26 +5,16 @@ */ import React from 'react'; -import { euiPaletteColorBlind } from '@elastic/eui'; -import { FieldIcon, typeToEuiIconMap } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon, FieldIconProps } from '../../../../../../src/plugins/kibana_react/public'; import { DataType } from '../types'; import { normalizeOperationDataType } from './utils'; -export function getColorForDataType(type: string) { - const iconMap = typeToEuiIconMap[normalizeOperationDataType(type as DataType)]; - if (iconMap) { - return iconMap.color; - } - return euiPaletteColorBlind()[0]; -} - -export function LensFieldIcon({ type }: { type: DataType }) { +export function LensFieldIcon({ type, ...rest }: FieldIconProps & { type: DataType }) { return ( ); } diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap index f37dfdd879c5b..d0cdbe7243abe 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap @@ -23,7 +23,7 @@ exports[`Should remove selected fields from selectable 1`] = ` id="addTooltipFieldPopover" isOpen={false} ownFocus={true} - panelPaddingSize="m" + panelPaddingSize="none" > , "value": "@timestamp", }, ] } + searchProps={ + Object { + "compressed": true, + } + } searchable={true} singleSelection={false} > @@ -88,7 +93,7 @@ exports[`Should render 1`] = ` id="addTooltipFieldPopover" isOpen={false} ownFocus={true} - panelPaddingSize="m" + panelPaddingSize="none" > , "value": "@timestamp", }, Object { "label": "custom label for prop1", "prepend": , "value": "prop1", }, Object { "label": "prop2", "prepend": , "value": "prop2", }, ] } + searchProps={ + Object { + "compressed": true, + } + } searchable={true} singleSelection={false} > diff --git a/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js b/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js index bddb74596f4ef..07bc54663c1d8 100644 --- a/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js +++ b/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js @@ -39,7 +39,10 @@ function getOptions(fields, selectedFields) { .map(field => { return { value: field.name, - prepend: 'type' in field ? : null, + prepend: + 'type' in field ? ( + + ) : null, label: 'label' in field ? field.label : field.name, }; }) @@ -127,7 +130,12 @@ export class AddTooltipFieldPopover extends Component { return ( - + {(list, search) => (
{search} @@ -161,6 +169,7 @@ export class AddTooltipFieldPopover extends Component { button={this._renderAddButton()} isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} + panelPaddingSize="none" ownFocus > {this._renderContent()} diff --git a/x-pack/legacy/plugins/maps/public/components/single_field_select.js b/x-pack/legacy/plugins/maps/public/components/single_field_select.js index 7351ce7691a82..98e33454b041b 100644 --- a/x-pack/legacy/plugins/maps/public/components/single_field_select.js +++ b/x-pack/legacy/plugins/maps/public/components/single_field_select.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox, EuiHighlight } from '@elastic/eui'; +import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; function fieldsToOptions(fields) { @@ -30,11 +30,14 @@ function fieldsToOptions(fields) { function renderOption(option, searchValue, contentClassName) { return ( - - -   - {option.label} - + + + + + + {option.label} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js index a32c2ce04d735..cf0ec5589d6bc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js @@ -7,18 +7,21 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox, EuiHighlight } from '@elastic/eui'; +import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FIELD_ORIGIN } from '../../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { FieldIcon } from '../../../../../../../../../src/plugins/kibana_react/public'; function renderOption(option, searchValue, contentClassName) { return ( - - -   - {option.label} - + + + + + + {option.label} + + ); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 87642d9f8bea8..863dbee7b5c8b 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -83,7 +83,11 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num } function getIndexPatternsWithGeoFieldCount(indexPatterns: IIndexPattern[]) { - const fieldLists = indexPatterns.map(indexPattern => JSON.parse(indexPattern.attributes.fields)); + const fieldLists = indexPatterns.map(indexPattern => + indexPattern.attributes && indexPattern.attributes.fields + ? JSON.parse(indexPattern.attributes.fields) + : [] + ); const fieldListsWithGeoFields = fieldLists.filter(fields => fields.some( (field: IFieldType) => diff --git a/x-pack/legacy/plugins/ml/common/types/kibana.ts b/x-pack/legacy/plugins/ml/common/types/kibana.ts index d647bd882162b..4a2edfebd1bac 100644 --- a/x-pack/legacy/plugins/ml/common/types/kibana.ts +++ b/x-pack/legacy/plugins/ml/common/types/kibana.ts @@ -6,7 +6,8 @@ // custom edits or fixes for default kibana types which are incomplete -import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; +import { SimpleSavedObject } from 'kibana/public'; +import { IndexPatternAttributes } from 'src/plugins/data/common'; export type IndexPatternTitle = string; @@ -17,8 +18,9 @@ export interface Route { k7Breadcrumbs: () => any; } -export type IndexPatternSavedObject = SimpleSavedObject; -export type SavedSearchSavedObject = SimpleSavedObject; +export type IndexPatternSavedObject = SimpleSavedObject; +// TODO define saved object type +export type SavedSearchSavedObject = SimpleSavedObject; export function isSavedSearchSavedObject( ss: SavedSearchSavedObject | null diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 0ef5e14e44f71..09f1b9ccedce4 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -81,7 +81,8 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, - elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, // NP + coreSavedObjects: kbnServer.newPlatform.start.core.savedObjects, + elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, }; const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; const plugins = { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index fa0ed34dca622..9c60b84b16bdf 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -61,7 +61,7 @@ export const checkForSavedObjects = async (objects: KibanaObjects): Promise { const acc = await prevPromise; - const { savedObjects } = await savedObjectsClient.find({ + const { savedObjects } = await savedObjectsClient.find({ type, perPage: 1000, }); diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 88b56b2329ae6..220d707ddd665 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -5,11 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { Query } from 'src/plugins/data/public'; import { IndexPattern, IIndexPattern, IndexPatternsContract, + Query, + IndexPatternAttributes, } from '../../../../../../../src/plugins/data/public'; import { getToastNotifications, getSavedObjectsClient } from './dependency_cache'; import { IndexPatternSavedObject, SavedSearchSavedObject } from '../../../common/types/kibana'; @@ -22,7 +23,7 @@ export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { indexPatternsContract = indexPatterns; const savedObjectsClient = getSavedObjectsClient(); return savedObjectsClient - .find({ + .find({ type: 'index-pattern', fields: ['id', 'title', 'type', 'fields'], perPage: 10000, diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts index 5da4f6b62bcec..dffd95f50e0d9 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts @@ -6,7 +6,6 @@ export { createMlTelemetry, - getSavedObjectsClient, incrementFileDataVisualizerIndexCreationCount, storeMlTelemetry, MlTelemetry, diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 293480b2aa5dc..a120450bbb2b0 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -5,19 +5,17 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { SavedObjectsServiceStart } from 'src/core/server'; import { createMlTelemetry, - getSavedObjectsClient, ML_TELEMETRY_DOC_ID, MlTelemetry, MlTelemetrySavedObject, } from './ml_telemetry'; -import { UsageInitialization } from '../../new_platform/plugin'; - export function makeMlUsageCollector( usageCollection: UsageCollectionSetup | undefined, - { elasticsearchPlugin, savedObjects }: UsageInitialization + savedObjects: SavedObjectsServiceStart ): void { if (!usageCollection) { return; @@ -28,11 +26,10 @@ export function makeMlUsageCollector( isReady: () => true, fetch: async (): Promise => { try { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - const mlTelemetrySavedObject = (await savedObjectsClient.get( - 'ml-telemetry', - ML_TELEMETRY_DOC_ID - )) as MlTelemetrySavedObject; + const mlTelemetrySavedObject: MlTelemetrySavedObject = await savedObjects + .createInternalRepository() + .get('ml-telemetry', ML_TELEMETRY_DOC_ID); + return mlTelemetrySavedObject.attributes; } catch (err) { return createMlTelemetry(); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts index fcf3763626b6f..9d14ffb31be63 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts @@ -6,7 +6,6 @@ import { createMlTelemetry, - getSavedObjectsClient, incrementFileDataVisualizerIndexCreationCount, ML_TELEMETRY_DOC_ID, MlTelemetry, @@ -26,22 +25,11 @@ describe('ml_telemetry', () => { }); describe('storeMlTelemetry', () => { - let elasticsearchPlugin: any; - let savedObjects: any; let mlTelemetry: MlTelemetry; - let savedObjectsClientInstance: any; + let internalRepository: any; beforeEach(() => { - savedObjectsClientInstance = { create: jest.fn() }; - const callWithInternalUser = jest.fn(); - const internalRepository = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; + internalRepository = { create: jest.fn(), get: jest.fn() }; mlTelemetry = { file_data_visualizer: { index_creation_count: 1, @@ -49,59 +37,28 @@ describe('ml_telemetry', () => { }; }); - it('should call savedObjectsClient create with the given MlTelemetry object', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe(mlTelemetry); + it('should call internalRepository create with the given MlTelemetry object', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][1]).toBe(mlTelemetry); }); - it('should call savedObjectsClient create with the ml-telemetry document type and ID', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); + it('should call internalRepository create with the ml-telemetry document type and ID', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); }); - it('should call savedObjectsClient create with overwrite: true', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe(true); - }); - }); - - describe('getSavedObjectsClient', () => { - let elasticsearchPlugin: any; - let savedObjects: any; - let savedObjectsClientInstance: any; - let callWithInternalUser: any; - let internalRepository: any; - - beforeEach(() => { - savedObjectsClientInstance = { create: jest.fn() }; - callWithInternalUser = jest.fn(); - internalRepository = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; - }); - - it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => { - const result = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - - expect(result).toBe(savedObjectsClientInstance); - expect(savedObjects.SavedObjectsClient).toHaveBeenCalledWith(internalRepository); + it('should call internalRepository create with overwrite: true', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][2].overwrite).toBe(true); }); }); describe('incrementFileDataVisualizerIndexCreationCount', () => { - let elasticsearchPlugin: any; let savedObjects: any; - let savedObjectsClientInstance: any; - let callWithInternalUser: any; let internalRepository: any; - function createSavedObjectsClientInstance( + function createInternalRepositoryInstance( telemetryEnabled?: boolean, indexCreationCount?: number ) { @@ -136,51 +93,42 @@ describe('ml_telemetry', () => { } function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - savedObjectsClientInstance = createSavedObjectsClientInstance( - telemetryEnabled, - indexCreationCount - ); - callWithInternalUser = jest.fn(); - internalRepository = jest.fn(); + internalRepository = createInternalRepositoryInstance(telemetryEnabled, indexCreationCount); savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), + createInternalRepository: jest.fn(() => internalRepository), }; } it('should not increment if telemetry status cannot be determined', async () => { mockInit(); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); + expect(internalRepository.create.mock.calls).toHaveLength(0); }); it('should not increment if telemetry status is disabled', async () => { mockInit(false); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); + expect(internalRepository.create.mock.calls).toHaveLength(0); }); it('should initialize index_creation_count with 1', async () => { mockInit(true); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 1 }, }); }); it('should increment index_creation_count to 2', async () => { mockInit(true, 1); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 2 }, }); }); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index 1bac3f1780644..d76b1ee94e21e 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; -import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory'; +import { + SavedObjectAttributes, + SavedObjectsServiceStart, + ISavedObjectsRepository, +} from 'src/core/server'; -export interface MlTelemetry { +export interface MlTelemetry extends SavedObjectAttributes { file_data_visualizer: { index_creation_count: number; }; @@ -29,35 +31,22 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { } // savedObjects export function storeMlTelemetry( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService, + internalRepository: ISavedObjectsRepository, mlTelemetry: MlTelemetry ): void { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - savedObjectsClient.create('ml-telemetry', mlTelemetry, { + internalRepository.create('ml-telemetry', mlTelemetry, { id: ML_TELEMETRY_DOC_ID, overwrite: true, }); } -// needs savedObjects and elasticsearchPlugin -export function getSavedObjectsClient( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService -): any { - const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects; - const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - return new SavedObjectsClient(internalRepository); -} export async function incrementFileDataVisualizerIndexCreationCount( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService + savedObjects: SavedObjectsServiceStart ): Promise { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - + const internalRepository = await savedObjects.createInternalRepository(); try { - const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry'); + const { attributes } = await internalRepository.get('telemetry', 'telemetry'); + if (attributes.enabled === false) { return; } @@ -70,7 +59,7 @@ export async function incrementFileDataVisualizerIndexCreationCount( let indicesCount = 1; try { - const { attributes } = (await savedObjectsClient.get( + const { attributes } = (await internalRepository.get( 'ml-telemetry', ML_TELEMETRY_DOC_ID )) as MlTelemetrySavedObject; @@ -80,5 +69,5 @@ export async function incrementFileDataVisualizerIndexCreationCount( } const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); + storeMlTelemetry(internalRepository, mlTelemetry); } diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts index b62e44c299a2d..553de75e38e05 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -8,6 +8,7 @@ import fs from 'fs'; import Boom from 'boom'; import numeral from '@elastic/numeral'; import { CallAPIOptions, RequestHandlerContext, SavedObjectsClientContract } from 'kibana/server'; +import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; import { MlJob } from '../../../common/types/jobs'; import { @@ -532,7 +533,10 @@ export class DataRecognizer { } async loadIndexPatterns() { - return await this.savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); + return await this.savedObjectsClient.find({ + type: 'index-pattern', + perPage: 1000, + }); } // returns a id based on an index pattern name @@ -620,7 +624,8 @@ export class DataRecognizer { // find all existing savedObjects for a given type loadExistingSavedObjects(type: string) { - return this.savedObjectsClient.find({ type, perPage: 1000 }); + // TODO: define saved object type + return this.savedObjectsClient.find({ type, perPage: 1000 }); } // save the savedObjects if they do not exist already diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts b/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts new file mode 100644 index 0000000000000..38f7dce93546d --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts @@ -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 { APICaller } from 'src/core/server'; + +export function fieldsServiceProvider( + callAsCurrentUser: APICaller +): { + getCardinalityOfFields: ( + index: string[] | string, + fieldNames: string[], + query: any, + timeFieldName: string, + earliestMs: number, + latestMs: number + ) => Promise; + getTimeFieldRange: (index: string[] | string, timeFieldName: string, query: any) => Promise; +}; diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js b/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js index 3b2de50ee2e63..a538693a92aba 100644 --- a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js +++ b/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js @@ -7,7 +7,7 @@ // Service for carrying out queries to obtain data // specific to fields in Elasticsearch indices. -export function fieldsServiceProvider(callWithRequest) { +export function fieldsServiceProvider(callAsCurrentUser) { // Obtains the cardinality of one or more fields. // Returns an Object whose keys are the names of the fields, // with values equal to the cardinality of the field. @@ -17,7 +17,7 @@ export function fieldsServiceProvider(callWithRequest) { // First check that each of the supplied fieldNames are aggregatable, // then obtain the cardinality for each of the aggregatable fields. return new Promise((resolve, reject) => { - callWithRequest('fieldCaps', { + callAsCurrentUser('fieldCaps', { index, fields: fieldNames, }) @@ -72,7 +72,7 @@ export function fieldsServiceProvider(callWithRequest) { aggs, }; - callWithRequest('search', { + callAsCurrentUser('search', { index, body, }) @@ -106,7 +106,7 @@ export function fieldsServiceProvider(callWithRequest) { return new Promise((resolve, reject) => { const obj = { success: true, start: { epoch: 0, string: '' }, end: { epoch: 0, string: '' } }; - callWithRequest('search', { + callAsCurrentUser('search', { index, size: 0, body: { diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/index.js b/x-pack/legacy/plugins/ml/server/models/fields_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/index.js rename to x-pack/legacy/plugins/ml/server/models/fields_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 11b0802192e1f..1e9ce3d8d5022 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -5,6 +5,7 @@ */ import { SavedObject } from 'src/core/server'; +import { IndexPatternAttributes } from 'src/plugins/data/server'; import { SavedObjectsClientContract } from 'kibana/server'; import { FieldId } from '../../../../common/types/fields'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; @@ -58,8 +59,8 @@ export async function rollupServiceProvider( async function loadRollupIndexPattern( indexPattern: string, savedObjectsClient: SavedObjectsClientContract -): Promise { - const resp = await savedObjectsClient.find({ +): Promise | null> { + const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type', 'typeMeta'], perPage: 1000, diff --git a/x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts new file mode 100644 index 0000000000000..e0fba498e0d58 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.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. + */ + +import { schema } from '@kbn/config-schema'; + +export const getCardinalityOfFieldsSchema = schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + fieldNames: schema.maybe(schema.arrayOf(schema.string())), + query: schema.maybe(schema.any()), + timeFieldName: schema.maybe(schema.string()), + earliestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + latestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), +}); + +export const getTimeFieldRangeSchema = schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + timeFieldName: schema.maybe(schema.string()), + query: schema.maybe(schema.any()), +}); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 10961182be841..43c276ac63a13 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -14,6 +14,7 @@ import { CoreSetup, IRouter, IScopedClusterClient, + SavedObjectsServiceStart, } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -28,12 +29,10 @@ import { LICENSE_TYPE } from '../../common/constants/license'; import { annotationRoutes } from '../routes/annotations'; import { jobRoutes } from '../routes/anomaly_detectors'; import { dataFeedRoutes } from '../routes/datafeeds'; -// @ts-ignore: could not find declaration file for module import { indicesRoutes } from '../routes/indices'; import { jobValidationRoutes } from '../routes/job_validation'; import { makeMlUsageCollector } from '../lib/ml_telemetry'; import { notificationRoutes } from '../routes/notification_settings'; -// @ts-ignore: could not find declaration file for module import { systemRoutes } from '../routes/system'; import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics'; import { dataRecognizer } from '../routes/modules'; @@ -45,7 +44,6 @@ import { filtersRoutes } from '../routes/filters'; import { resultsServiceRoutes } from '../routes/results_service'; import { jobServiceRoutes } from '../routes/job_service'; import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; -// @ts-ignore: could not find declaration file for module import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; import { initMlServerLog, LogInitialization } from '../client/log'; import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server'; @@ -67,6 +65,7 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; + coreSavedObjects: SavedObjectsServiceStart; elasticsearch: ElasticsearchServiceSetup; } export interface MlInitializerContext extends PluginInitializerContext { @@ -93,15 +92,11 @@ export interface RouteInitialization { route(route: ServerRoute | ServerRoute[]): void; router: IRouter; xpackMainPlugin: MlXpackMainPlugin; - savedObjects?: SavedObjectsLegacyService; + savedObjects?: SavedObjectsServiceStart; spacesPlugin: any; securityPlugin: any; cloud?: CloudSetup; } -export interface UsageInitialization { - elasticsearchPlugin: ElasticsearchPlugin; - savedObjects: SavedObjectsLegacyService; -} declare module 'kibana/server' { interface RequestHandlerContext { @@ -123,7 +118,7 @@ export class Plugin { public setup(core: MlCoreSetup, plugins: PluginsSetup) { const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http } = core; + const { http, coreSavedObjects } = core; const pluginId = this.pluginId; mirrorPluginStatus(xpackMainPlugin, plugins.ml); @@ -208,14 +203,10 @@ export class Plugin { const extendedRouteInitializationDeps: RouteInitialization = { ...routeInitializationDeps, config: this.config, - savedObjects: core.savedObjects, + savedObjects: coreSavedObjects, spacesPlugin: plugins.spaces, cloud: plugins.cloud, }; - const usageInitializationDeps: UsageInitialization = { - elasticsearchPlugin: plugins.elasticsearch, - savedObjects: core.savedObjects, - }; const logInitializationDeps: LogInitialization = { log: this.log, @@ -240,7 +231,7 @@ export class Plugin { fileDataVisualizerRoutes(extendedRouteInitializationDeps); initMlServerLog(logInitializationDeps); - makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps); + makeMlUsageCollector(plugins.usageCollection, coreSavedObjects); } public stop() {} diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 7d1f13ead3fef..946e3bd71d6c3 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -105,6 +105,9 @@ "DeleteDatafeed", "StartDatafeed", "StopDatafeed", - "PreviewDatafeed" + "PreviewDatafeed", + "FieldsService", + "GetCardinalityOfFields", + "GetTimeFieldRange" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/fields_service.js b/x-pack/legacy/plugins/ml/server/routes/fields_service.js deleted file mode 100644 index 7848ffbd8bafe..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/fields_service.js +++ /dev/null @@ -1,49 +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 { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { fieldsServiceProvider } from '../models/fields_service'; - -function getCardinalityOfFields(callWithRequest, payload) { - const fs = fieldsServiceProvider(callWithRequest); - const { index, fieldNames, query, timeFieldName, earliestMs, latestMs } = payload; - return fs.getCardinalityOfFields(index, fieldNames, query, timeFieldName, earliestMs, latestMs); -} - -function getTimeFieldRange(callWithRequest, payload) { - const fs = fieldsServiceProvider(callWithRequest); - const { index, timeFieldName, query } = payload; - return fs.getTimeFieldRange(index, timeFieldName, query); -} - -export function fieldsService({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/fields_service/field_cardinality', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return getCardinalityOfFields(callWithRequest, request.payload).catch(resp => - wrapError(resp) - ); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/fields_service/time_field_range', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - return getTimeFieldRange(callWithRequest, request.payload).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/fields_service.ts b/x-pack/legacy/plugins/ml/server/routes/fields_service.ts new file mode 100644 index 0000000000000..4827adf23d7b4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/fields_service.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'src/core/server'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { wrapError } from '../client/error_wrapper'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + getCardinalityOfFieldsSchema, + getTimeFieldRangeSchema, +} from '../new_platform/fields_service_schema'; +import { fieldsServiceProvider } from '../models/fields_service'; + +function getCardinalityOfFields(context: RequestHandlerContext, payload: any) { + const fs = fieldsServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { index, fieldNames, query, timeFieldName, earliestMs, latestMs } = payload; + return fs.getCardinalityOfFields(index, fieldNames, query, timeFieldName, earliestMs, latestMs); +} + +function getTimeFieldRange(context: RequestHandlerContext, payload: any) { + const fs = fieldsServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { index, timeFieldName, query } = payload; + return fs.getTimeFieldRange(index, timeFieldName, query); +} + +/** + * Routes for fields service + */ +export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup FieldsService + * + * @api {post} /api/ml/fields_service/field_cardinality Get cardinality of fields + * @apiName GetCardinalityOfFields + * @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field + */ + router.post( + { + path: '/api/ml/fields_service/field_cardinality', + validate: { + body: getCardinalityOfFieldsSchema, + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const resp = await getCardinalityOfFields(context, request.body); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup FieldsService + * + * @api {post} /api/ml/fields_service/time_field_range Get time field range + * @apiName GetTimeFieldRange + * @apiDescription Returns the timefield range for the given index + */ + router.post( + { + path: '/api/ml/fields_service/time_field_range', + validate: { + body: getTimeFieldRangeSchema, + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const resp = await getTimeFieldRange(context, request.body); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts index 95f2a9fe7298f..d5a992c933293 100644 --- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts @@ -138,7 +138,7 @@ export function fileDataVisualizerRoutes({ // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects!); + await incrementFileDataVisualizerIndexCreationCount(savedObjects!); } const result = await importData( diff --git a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts index 7298312990005..76986b935b993 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts @@ -50,7 +50,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial /** * @apiGroup JobAuditMessages * - * @api {get} /api/ml/results/anomalies_table_data Get all audit messages + * @api {get} /api/ml/job_audit_messages/messages Get all audit messages * @apiName GetAllJobAuditMessages * @apiDescription Returns all audit messages */ diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts index f968e90f70b2d..9ef19e58bada7 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -54,6 +54,7 @@ export const getLicenseExpiration = ( }), }, ], + defaultActionGroupId: 'default', async executor({ services, params, diff --git a/x-pack/legacy/plugins/siem/cypress.json b/x-pack/legacy/plugins/siem/cypress.json index ae73965f8f10f..d2397e1ec90dd 100644 --- a/x-pack/legacy/plugins/siem/cypress.json +++ b/x-pack/legacy/plugins/siem/cypress.json @@ -1,5 +1,6 @@ { "baseUrl": "http://localhost:5601", + "defaultCommandTimeout": 30000, "screenshotsFolder": "../../../../target/kibana-siem/cypress/screenshots", "trashAssetsBeforeRuns": false, "video": false, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts similarity index 83% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts index 7bb7b9f4da5d1..e44c8f4459ba9 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/events_viewer/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts @@ -4,38 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { filterFieldsBrowser } from '../../lib/fields_browser/helpers'; import { + FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_TITLE, - FIELDS_BROWSER_CHECKBOX, -} from '../../../screens/hosts/fields_browser'; -import { HOSTS_PAGE } from '../../lib/urls'; -import { loginAndWaitForPage } from '../../../tasks/login'; -import { openEventsViewerFieldsBrowser, filterSearchBar } from '../../lib/events_viewer/helpers'; -import { closeFieldsBrowser } from '../../../tasks/hosts/fields_browsers'; -import { openEvents } from '../../../tasks/hosts/main'; -import { - closeModal, - opensInspectQueryModal, - waitsForEventsToBeLoaded, - addsHostGeoCityNameToHeader, - addsHostGeoCountryNameToHeader, - resetFields, -} from '../../../tasks/hosts/events'; - +} from '../screens/fields_browser'; import { HEADER_SUBTITLE, + HOST_GEO_CITY_NAME_HEADER, + HOST_GEO_COUNTRY_NAME_HEADER, INSPECT_MODAL, LOAD_MORE, LOCAL_EVENTS_COUNT, - HOST_GEO_CITY_NAME_HEADER, - HOST_GEO_COUNTRY_NAME_HEADER, -} from '../../../screens/hosts/events'; -import { DEFAULT_TIMEOUT } from '../../lib/util/helpers'; +} from '../screens/hosts/events'; + +import { closeFieldsBrowser, filterFieldsBrowser } from '../tasks/fields_browser'; +import { loginAndWaitForPage } from '../tasks/login'; +import { openEvents } from '../tasks/hosts/main'; +import { + addsHostGeoCityNameToHeader, + addsHostGeoCountryNameToHeader, + closeModal, + openEventsViewerFieldsBrowser, + opensInspectQueryModal, + resetFields, + waitsForEventsToBeLoaded, +} from '../tasks/hosts/events'; +import { clearSearchBar, kqlSearch } from '../tasks/siem_header'; -import { clearSearchBar } from '../../../tasks/header'; +import { HOSTS_PAGE } from '../urls/navigation'; const defaultHeadersInDefaultEcsCategory = [ { id: '@timestamp' }, @@ -90,13 +88,13 @@ describe('Events Viewer', () => { after(() => { closeModal(); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('not.exist'); + cy.get(INSPECT_MODAL).should('not.exist'); }); it('launches the inspect query modal when the inspect button is clicked', () => { waitsForEventsToBeLoaded(); opensInspectQueryModal(); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('exist'); + cy.get(INSPECT_MODAL).should('exist'); }); }); @@ -147,7 +145,7 @@ describe('Events Viewer', () => { cy.get(HEADER_SUBTITLE) .invoke('text') .then(initialNumberOfEvents => { - filterSearchBar(filterInput); + kqlSearch(`${filterInput}{enter}`); cy.get(HEADER_SUBTITLE) .invoke('text') .should('not.equal', initialNumberOfEvents); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts similarity index 87% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts index 6e8ef93a54016..095fc30356fd4 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts @@ -4,38 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE } from '../../lib/urls'; - -import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../../tasks/login'; - import { - FIELDS_BROWSER_TITLE, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, FIELDS_BROWSER_CATEGORIES_COUNT, - FIELDS_BROWSER_HOST_CATEGORIES_COUNT, - FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_FIELDS_COUNT, - FIELDS_BROWSER_MESSAGE_HEADER, + FIELDS_BROWSER_HOST_CATEGORIES_COUNT, FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER, FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, -} from '../../../screens/timeline/fields_browser'; - -import { populateTimeline, openTimelineFieldsBrowser } from '../../../tasks/timeline/main'; - -import { openTimeline } from '../../../tasks/siem_main'; + FIELDS_BROWSER_MESSAGE_HEADER, + FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, + FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, + FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, + FIELDS_BROWSER_TITLE, +} from '../screens/fields_browser'; import { + addsHostGeoCityNameToTimeline, + addsHostGeoContinentNameToTimeline, + addsHostGeoCountryNameToTimelineDraggingIt, clearFieldsBrowser, - filterFieldsBrowser, closeFieldsBrowser, + filterFieldsBrowser, removesMessageField, - addsHostGeoCityNameToTimeline, - addsHostGeoCountryNameToTimelineDraggingIt, - addsHostGeoContinentNameToTimeline, resetFields, -} from '../../../tasks/timeline/fields_browser'; +} from '../tasks/fields_browser'; +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline } from '../tasks/siem_main'; +import { openTimelineFieldsBrowser, populateTimeline } from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; const defaultHeaders = [ { id: '@timestamp' }, @@ -90,7 +87,7 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_CATEGORIES_COUNT, { timeout: DEFAULT_TIMEOUT }) + cy.get(FIELDS_BROWSER_CATEGORIES_COUNT) .invoke('text') .should('eq', '2 categories'); }); @@ -106,7 +103,7 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) .invoke('text') .then(systemCategoriesCount => { - cy.get(FIELDS_BROWSER_FIELDS_COUNT, { timeout: DEFAULT_TIMEOUT }) + cy.get(FIELDS_BROWSER_FIELDS_COUNT) .invoke('text') .should('eq', `${+hostCategoriesCount + +systemCategoriesCount} fields`); }); @@ -163,9 +160,7 @@ describe('Fields Browser', () => { addsHostGeoCityNameToTimeline(); closeFieldsBrowser(); - cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, { - timeout: DEFAULT_TIMEOUT, - }).should('exist'); + cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('exist'); }); it('adds a field to the timeline when the user drags and drops a field', () => { @@ -177,9 +172,7 @@ describe('Fields Browser', () => { addsHostGeoCountryNameToTimelineDraggingIt(); - cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER, { - timeout: DEFAULT_TIMEOUT, - }).should('exist'); + cy.get(FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER).should('exist'); }); it('resets all fields in the timeline when `Reset Fields` is clicked', () => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/inspect.spec.ts similarity index 70% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/inspect.spec.ts index 1555470f5eee7..b6b4e7a72b8f6 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/inspect.spec.ts @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE, NETWORK_PAGE } from '../../lib/urls'; import { + INSPECT_HOSTS_BUTTONS_IN_SIEM, INSPECT_MODAL, INSPECT_NETWORK_BUTTONS_IN_SIEM, - INSPECT_HOSTS_BUTTONS_IN_SIEM, -} from '../../../screens/inspect'; +} from '../screens/inspect'; + +import { closesModal, openStatsAndTables } from '../tasks/inspect'; +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline } from '../tasks/siem_main'; import { executeTimelineKQL, - openTimelineSettings, openTimelineInspectButton, -} from '../../../tasks/timeline/main'; -import { openTimeline } from '../../../tasks/siem_main'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; -import { closesModal, openStatsAndTables } from '../../../tasks/inspect'; + openTimelineSettings, +} from '../tasks/timeline'; + +import { HOSTS_PAGE, NETWORK_PAGE } from '../urls/navigation'; describe('Inspect', () => { context('Hosts stats and tables', () => { @@ -31,7 +33,7 @@ describe('Inspect', () => { INSPECT_HOSTS_BUTTONS_IN_SIEM.forEach(table => it(`inspects the ${table.title}`, () => { openStatsAndTables(table); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('be.visible'); + cy.get(INSPECT_MODAL).should('be.visible'); }) ); }); @@ -47,7 +49,7 @@ describe('Inspect', () => { INSPECT_NETWORK_BUTTONS_IN_SIEM.forEach(table => it(`inspects the ${table.title}`, () => { openStatsAndTables(table); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('be.visible'); + cy.get(INSPECT_MODAL).should('be.visible'); }) ); }); @@ -60,7 +62,7 @@ describe('Inspect', () => { executeTimelineKQL(hostExistsQuery); openTimelineSettings(); openTimelineInspectButton(); - cy.get(INSPECT_MODAL, { timeout: DEFAULT_TIMEOUT }).should('be.visible'); + cy.get(INSPECT_MODAL).should('be.visible'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts deleted file mode 100644 index 39a61401c15b3..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts +++ /dev/null @@ -1,50 +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. - */ - -const primaryButton = 0; - -/** - * To overcome the React Beautiful DND sloppy click detection threshold: - * https://github.com/atlassian/react-beautiful-dnd/blob/67b96c8d04f64af6b63ae1315f74fc02b5db032b/docs/sensors/mouse.md#sloppy-clicks-and-click-prevention- - */ -const dndSloppyClickDetectionThreshold = 5; - -/** Starts dragging the subject */ -export const drag = (subject: JQuery) => { - const subjectLocation = subject[0].getBoundingClientRect(); - - cy.wrap(subject) - .trigger('mousedown', { - button: primaryButton, - clientX: subjectLocation.left, - clientY: subjectLocation.top, - force: true, - }) - .wait(1) - .trigger('mousemove', { - button: primaryButton, - clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, - clientY: subjectLocation.top, - force: true, - }) - .wait(1); -}; - -/** "Drops" the subject being dragged on the specified drop target */ -export const drop = (dropTarget: JQuery) => { - cy.wrap(dropTarget) - .trigger('mousemove', { button: primaryButton, force: true }) - .wait(1) - .trigger('mouseup', { force: true }) - .wait(1); -}; - -/** Drags the subject being dragged on the specified drop target, but does not drop it */ -export const dragWithoutDrop = (dropTarget: JQuery) => { - cy.wrap(dropTarget).trigger('mousemove', 'center', { - button: primaryButton, - }); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/helpers.ts deleted file mode 100644 index 02a80f6c0329c..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EVENTS_VIEWER_FIELDS_BUTTON, KQL_SEARCH_BAR } from './selectors'; -import { FIELDS_BROWSER_CONTAINER } from '../fields_browser/selectors'; -import { SERVER_SIDE_EVENT_COUNT } from '../timeline/selectors'; -import { DEFAULT_TIMEOUT } from '../util/helpers'; - -/** Opens the eventsViewer Field Browser */ -export const openEventsViewerFieldsBrowser = () => { - cy.get(EVENTS_VIEWER_FIELDS_BUTTON, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); - - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) - .invoke('text') - .should('not.equal', '0'); - - cy.get(FIELDS_BROWSER_CONTAINER).should('exist'); -}; - -/** Clicks an arbitrary UI element that's not part of the fields browser (to dismiss it) */ -export const clickOutsideFieldsBrowser = () => { - cy.get(KQL_SEARCH_BAR, { timeout: DEFAULT_TIMEOUT }).click(); -}; - -/** Filters the search bar at the top of most pages with the specified KQL */ -export const filterSearchBar = (kql: string) => { - cy.get(KQL_SEARCH_BAR).type(`${kql} {enter}`); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts deleted file mode 100644 index 6f7906d7fd791..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/events_viewer/selectors.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** The panel containing the events viewer */ -export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]'; - -/** Clicking this button in the timeline opens the Fields browser in the Events Viewer */ -export const EVENTS_VIEWER_FIELDS_BUTTON = `${EVENTS_VIEWER_PANEL} [data-test-subj="show-field-browser-gear"]`; - -/** The KQL search bar that exists at the top of most pages */ -export const KQL_SEARCH_BAR = '[data-test-subj="queryInput"]'; - -/** The Events Viewer Showing N events header subtitle */ -export const HEADER_SUBTITLE = `${EVENTS_VIEWER_PANEL} [data-test-subj="header-panel-subtitle"]`; - -/** The inspect query modal */ -export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; - -export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; - -/** The inspect query button that launches the inspect query modal */ -export const INSPECT_QUERY = `${EVENTS_VIEWER_PANEL} [data-test-subj="inspect-icon-button"]`; - -/** A count of the events loaded in the table */ -export const LOCAL_EVENTS_COUNT = `${EVENTS_VIEWER_PANEL} [data-test-subj="local-events-count"]`; - -/** The events viewer Load More button */ -export const LOAD_MORE = `${EVENTS_VIEWER_PANEL} [data-test-subj="TimelineMoreButton"]`; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts deleted file mode 100644 index b02eda513cba3..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts +++ /dev/null @@ -1,44 +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 { FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_FILTER_INPUT } from './selectors'; -import { - assertAtLeastOneEventMatchesSearch, - executeKQL, - hostExistsQuery, - toggleTimelineVisibility, -} from '../timeline/helpers'; -import { TIMELINE_DATA_PROVIDERS, TIMELINE_FIELDS_BUTTON } from '../timeline/selectors'; - -/** Opens the timeline's Field Browser */ -export const openTimelineFieldsBrowser = () => { - cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); - - cy.get(FIELDS_BROWSER_CONTAINER).should('exist'); -}; - -/** Populates the timeline with a host from the hosts page */ -export const populateTimeline = () => { - toggleTimelineVisibility(); - - executeKQL(hostExistsQuery); - - assertAtLeastOneEventMatchesSearch(); -}; - -/** Clicks an arbitrary UI element that's not part of the fields browser (to dismiss it) */ -export const clickOutsideFieldsBrowser = () => { - cy.get(TIMELINE_DATA_PROVIDERS).click(); -}; - -/** Filters the Field Browser by typing `fieldName` in the input */ -export const filterFieldsBrowser = (fieldName: string) => { - cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName); -}; - -export const clearFieldsBrowser = () => { - cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/selectors.ts deleted file mode 100644 index 039e38aaf3ee7..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/selectors.ts +++ /dev/null @@ -1,40 +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. - */ - -/** Clicking this button in the timeline opens the Fields browser */ -export const TIMELINE_FIELDS_BUTTON = - '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; - -/** The title displayed in the fields browser (i.e. Customize Columns) */ -export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]'; - -/** Contains the body of the fields browser */ -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; - -/** The title of the selected category in the right-hand side of the fields browser */ -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]'; - -/** A count of the fields in the selected category in the right-hand side of the fields browser */ -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = - '[data-test-subj="selected-category-count-badge"]'; - -/** Typing in this input filters the Field Browser */ -export const FIELDS_BROWSER_FILTER_INPUT = '[data-test-subj="field-search"]'; - -/** - * This label displays a count of the categories containing (one or more) - * fields that match the filter criteria - */ -export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; - -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = '[data-test-subj="host-category-count"]'; - -export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = '[data-test-subj="system-category-count"]'; - -/** - * This label displays a count of the fields that match the filter criteria - */ -export const FIELDS_BROWSER_FIELDS_COUNT = '[data-test-subj="fields-count"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/fixtures/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/fixtures/helpers.ts deleted file mode 100644 index e66199e0419ec..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/fixtures/helpers.ts +++ /dev/null @@ -1,19 +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. - */ - -// Cypress workaround to hijack XHR -// https://github.com/cypress-io/cypress/issues/687 -export const clearFetch = () => - cy.on('window:before:load', win => { - // @ts-ignore no null, this is a temp hack see issue above - win.fetch = null; - }); - -export const stubApi = (dataFileName: string) => { - cy.server(); - cy.fixture(dataFileName).as(`${dataFileName}JSON`); - cy.route('POST', 'api/siem/graphql', `@${dataFileName}JSON`); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/helpers.ts deleted file mode 100644 index 8f4efbe0c09c2..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/helpers.ts +++ /dev/null @@ -1,16 +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 { DEFAULT_TIMEOUT } from '../util/helpers'; - -import { ALL_HOSTS_WIDGET, EVENTS_TAB_BUTTON } from './selectors'; - -/** Wait for the for the `All Hosts` widget on the `Hosts` page to load */ -export const waitForAllHostsWidget = () => cy.get(ALL_HOSTS_WIDGET, { timeout: DEFAULT_TIMEOUT }); - -/** Clicks the Events tab on the hosts page */ -export const clickEventsTab = () => - cy.get(EVENTS_TAB_BUTTON, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts deleted file mode 100644 index ab2502676d6f7..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts +++ /dev/null @@ -1,21 +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. - */ - -/** The `All Hosts` widget on the `Hosts` page */ -export const ALL_HOSTS_WIDGET = '[data-test-subj="table-allHosts-loading-false"]'; - -/** A single draggable host in the `All Hosts` widget on the `Hosts` page */ -export const ALL_HOSTS_WIDGET_HOST = '[data-test-subj="draggable-content-host.name"]'; - -/** All the draggable hosts in the `All Hosts` widget on the `Hosts` page */ -export const ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS = `${ALL_HOSTS_WIDGET} ${ALL_HOSTS_WIDGET_HOST}`; - -/** Clicking this button displays the `Events` tab */ -export const EVENTS_TAB_BUTTON = '[data-test-subj="navigation-events"]'; - -export const NAVIGATION_HOSTS_ALL_HOSTS = '[data-test-subj="navigation-allHosts"]'; - -export const NAVIGATION_HOSTS_ANOMALIES = '[data-test-subj="navigation-anomalies"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/helpers.ts deleted file mode 100644 index c431d0551b29a..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/helpers.ts +++ /dev/null @@ -1,29 +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 { DEFAULT_TIMEOUT } from '../util/helpers'; - -import { INSPECT_BUTTON_ICON, InspectButtonMetadata } from './selectors'; - -export const openStatsAndTables = (table: InspectButtonMetadata) => { - if (table.tabId) { - cy.get(table.tabId).click({ force: true }); - } - cy.get(table.id, { timeout: DEFAULT_TIMEOUT }); - if (table.altInspectId) { - cy.get(table.altInspectId, { timeout: DEFAULT_TIMEOUT }).trigger('click', { - force: true, - }); - } else { - cy.get(`${table.id} ${INSPECT_BUTTON_ICON}`, { - timeout: DEFAULT_TIMEOUT, - }).trigger('click', { force: true }); - } -}; - -export const closesModal = () => { - cy.get('[data-test-subj="modal-inspect-close"]', { timeout: DEFAULT_TIMEOUT }).click(); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/selectors.ts deleted file mode 100644 index a6d4b37be9f00..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/inspect/selectors.ts +++ /dev/null @@ -1,89 +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. - */ - -export const INSPECT_BUTTON_ICON = '[data-test-subj="inspect-icon-button"]'; -export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; -export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; -export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; - -export interface InspectButtonMetadata { - altInspectId?: string; - id: string; - title: string; - tabId?: string; -} - -export const INSPECT_HOSTS_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ - { - id: '[data-test-subj="stat-hosts"]', - title: 'Hosts Stat', - }, - { - id: '[data-test-subj="stat-authentication"]', - title: 'User Authentications Stat', - }, - { - id: '[data-test-subj="stat-uniqueIps"]', - title: 'Unique IPs Stat', - }, - { - id: '[data-test-subj="table-allHosts-loading-false"]', - title: 'All Hosts Table', - tabId: '[data-test-subj="navigation-allHosts"]', - }, - { - id: '[data-test-subj="table-authentications-loading-false"]', - title: 'Authentications Table', - tabId: '[data-test-subj="navigation-authentications"]', - }, - { - id: '[data-test-subj="table-uncommonProcesses-loading-false"]', - title: 'Uncommon processes Table', - tabId: '[data-test-subj="navigation-uncommonProcesses"]', - }, - { - altInspectId: `[data-test-subj="events-viewer-panel"] ${INSPECT_BUTTON_ICON}`, - id: '[data-test-subj="events-container-loading-false"]', - title: 'Events Table', - tabId: '[data-test-subj="navigation-events"]', - }, -]; - -export const INSPECT_NETWORK_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ - { - id: '[data-test-subj="stat-networkEvents"]', - title: 'Network events Stat', - }, - { - id: '[data-test-subj="stat-dnsQueries"]', - title: 'DNS queries Stat', - }, - { - id: '[data-test-subj="stat-uniqueFlowId"]', - title: 'Unique flow IDs Stat', - }, - { - id: '[data-test-subj="stat-tlsHandshakes"]', - title: 'TLS handshakes Stat', - }, - { - id: '[data-test-subj="stat-UniqueIps"]', - title: 'Unique private IPs Stat', - }, - { - id: '[data-test-subj="table-topNFlowSource-loading-false"]', - title: 'Source IPs Table', - }, - { - id: '[data-test-subj="table-topNFlowDestination-loading-false"]', - title: 'Destination IPs Table', - }, - { - id: '[data-test-subj="table-dns-loading-false"]', - title: 'Top DNS Domains Table', - tabId: '[data-test-subj="navigation-dns"]', - }, -]; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts deleted file mode 100644 index b2b8ce7b9c000..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts +++ /dev/null @@ -1,115 +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 * as yaml from 'js-yaml'; - -/** - * Credentials in the `kibana.dev.yml` config file will be used to authenticate - * with Kibana when credentials are not provided via environment variables - */ -const KIBANA_DEV_YML_PATH = '../../../../config/kibana.dev.yml'; - -/** - * The configuration path in `kibana.dev.yml` to the username to be used when - * authenticating with Kibana. - */ -const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username'; - -/** - * The configuration path in `kibana.dev.yml` to the password to be used when - * authenticating with Kibana. - */ -const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password'; - -/** - * The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the - * username to be used when authenticating with Kibana - */ -const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME'; - -/** - * The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the - * username to be used when authenticating with Kibana - */ -const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; - -/** - * The Kibana server endpoint used for authentication - */ -const LOGIN_API_ENDPOINT = '/internal/security/login'; - -/** - * Authenticates with Kibana using, if specified, credentials specified by - * environment variables. The credentials in `kibana.dev.yml` will be used - * for authentication when the environment variables are unset. - * - * To speed the execution of tests, prefer this non-interactive authentication, - * which is faster than authentication via Kibana's interactive login page. - */ -export const login = () => { - if (credentialsProvidedByEnvironment()) { - loginViaEnvironmentCredentials(); - } else { - loginViaConfig(); - } -}; - -/** - * Returns `true` if the credentials used to login to Kibana are provided - * via environment variables - */ -const credentialsProvidedByEnvironment = (): boolean => - Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null; - -/** - * Authenticates with Kibana by reading credentials from the - * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` - * environment variables, and POSTing the username and password directly to - * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). - */ -const loginViaEnvironmentCredentials = () => { - cy.log( - `Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables` - ); - - // programmatically authenticate without interacting with the Kibana login page - cy.request({ - body: { - username: Cypress.env(ELASTICSEARCH_USERNAME), - password: Cypress.env(ELASTICSEARCH_PASSWORD), - }, - headers: { 'kbn-xsrf': 'cypress-creds-via-env' }, - method: 'POST', - url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, - }); -}; - -/** - * Authenticates with Kibana by reading credentials from the - * `kibana.dev.yml` file and POSTing the username and password directly to - * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). - */ -const loginViaConfig = () => { - cy.log( - `Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\`` - ); - - // read the login details from `kibana.dev.yaml` - cy.readFile(KIBANA_DEV_YML_PATH).then(kibanaDevYml => { - const config = yaml.safeLoad(kibanaDevYml); - - // programmatically authenticate without interacting with the Kibana login page - cy.request({ - body: { - username: config.elasticsearch.username, - password: config.elasticsearch.password, - }, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'POST', - url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, - }); - }); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/selectors.ts deleted file mode 100644 index ac028fa91216c..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/selectors.ts +++ /dev/null @@ -1,14 +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. - */ - -/** The Username field in the Kibana login page */ -export const USERNAME = '[data-test-subj="loginUsername"]'; - -/** The Password field in the Kibana login page */ -export const PASSWORD = '[data-test-subj="loginPassword"]'; - -/** The `Default` space button on the `Select your space` page */ -export const DEFAULT_SPACE_BUTTON = '[data-test-subj="space-card-default"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/ml_conditional_links/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/ml_conditional_links/index.ts deleted file mode 100644 index 655418fc98bf8..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/ml_conditional_links/index.ts +++ /dev/null @@ -1,76 +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. - */ - -/* - * These links are for different test scenarios that try and capture different drill downs into - * ml-network and ml-hosts and are of the flavor of testing: - * A filter being null: (query:!n) - * A filter being set with single values: query=(query:%27process.name%20:%20%22conhost.exe%22%27,language:kuery) - * A filter being set with multiple values: query=(query:%27process.name%20:%20%22conhost.exe,sc.exe%22%27,language:kuery) - * A filter containing variables not replaced: query=(query:%27process.name%20:%20%$process.name$%22%27,language:kuery) - * - * In different combination with: - * network not being set: $ip$ - * host not being set: $host.name$ - * ...or... - * network being set normally: 127.0.0.1 - * host being set normally: suricata-iowa - * ...or... - * network having multiple values: 127.0.0.1,127.0.0.2 - * host having multiple values: suricata-iowa,siem-windows - */ - -// Single IP with a null for the Query: -export const mlNetworkSingleIpNullKqlQuery = - "/app/siem#/ml-network/ip/127.0.0.1?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// Single IP with a value for the Query: -export const mlNetworkSingleIpKqlQuery = - "/app/siem#/ml-network/ip/127.0.0.1?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// Multiple IPs with a null for the Query: -export const mlNetworkMultipleIpNullKqlQuery = - "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// Multiple IPs with a value for the Query: -export const mlNetworkMultipleIpKqlQuery = - "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// $ip$ with a null Query: -export const mlNetworkNullKqlQuery = - "/app/siem#/ml-network/ip/$ip$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// $ip$ with a value for the Query: -export const mlNetworkKqlQuery = - "/app/siem#/ml-network/ip/$ip$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; - -// Single host name with a null for the Query: -export const mlHostSingleHostNullKqlQuery = - "/app/siem#/ml-hosts/siem-windows?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Single host name with a variable in the Query: -export const mlHostSingleHostKqlQueryVariable = - "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22$process.name$%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Single host name with a value for Query: -export const mlHostSingleHostKqlQuery = - "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Multiple host names with null for Query: -export const mlHostMultiHostNullKqlQuery = - "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=!n&&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Multiple host names with a value for Query: -export const mlHostMultiHostKqlQuery = - "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Undefined/null host name with a null for the KQL: -export const mlHostVariableHostNullKqlQuery = - "/app/siem#/ml-hosts/$host.name$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; - -// Undefined/null host name but with a value for Query: -export const mlHostVariableHostKqlQuery = - "/app/siem#/ml-hosts/$host.name$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/navigation/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/navigation/selectors.ts deleted file mode 100644 index 0d5f40ae53966..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/navigation/selectors.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. - */ - -/** Top-level (global) navigation link to the `Hosts` page */ -export const NAVIGATION_HOSTS = '[data-test-subj="navigation-hosts"]'; - -/** Top-level (global) navigation link to the `Network` page */ -export const NAVIGATION_NETWORK = '[data-test-subj="navigation-network"]'; - -/** Top-level (global) navigation link to the `Overview` page */ -export const NAVIGATION_OVERVIEW = '[data-test-subj="navigation-overview"]'; - -/** Top-level (global) navigation link to the `Timelines` page */ -export const NAVIGATION_TIMELINES = '[data-test-subj="navigation-timelines"]'; - -export const HOSTS_PAGE_TABS = { - allHosts: '[data-test-subj="navigation-allHosts"]', - anomalies: '[data-test-subj="navigation-anomalies"]', - authentications: '[data-test-subj="navigation-authentications"]', - events: '[data-test-subj="navigation-events"]', - uncommonProcesses: '[data-test-subj="navigation-uncommonProcesses"]', -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts deleted file mode 100644 index 95facc8974400..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/overview/selectors.ts +++ /dev/null @@ -1,144 +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. - */ - -// Host Stats -export const STAT_AUDITD = { - value: '123', - domId: '[data-test-subj="host-stat-auditbeatAuditd"]', -}; -export const ENDGAME_DNS = { - value: '391', - domId: '[data-test-subj="host-stat-endgameDns"]', -}; -export const ENDGAME_FILE = { - value: '392', - domId: '[data-test-subj="host-stat-endgameFile"]', -}; -export const ENDGAME_IMAGE_LOAD = { - value: '393', - domId: '[data-test-subj="host-stat-endgameImageLoad"]', -}; -export const ENDGAME_NETWORK = { - value: '394', - domId: '[data-test-subj="host-stat-endgameNetwork"]', -}; -export const ENDGAME_PROCESS = { - value: '395', - domId: '[data-test-subj="host-stat-endgameProcess"]', -}; -export const ENDGAME_REGISTRY = { - value: '396', - domId: '[data-test-subj="host-stat-endgameRegistry"]', -}; -export const ENDGAME_SECURITY = { - value: '397', - domId: '[data-test-subj="host-stat-endgameSecurity"]', -}; -export const STAT_FILEBEAT = { - value: '890', - domId: '[data-test-subj="host-stat-filebeatSystemModule"]', -}; -export const STAT_FIM = { - value: '345', - domId: '[data-test-subj="host-stat-auditbeatFIM"]', -}; -export const STAT_LOGIN = { - value: '456', - domId: '[data-test-subj="host-stat-auditbeatLogin"]', -}; -export const STAT_PACKAGE = { - value: '567', - domId: '[data-test-subj="host-stat-auditbeatPackage"]', -}; -export const STAT_PROCESS = { - value: '678', - domId: '[data-test-subj="host-stat-auditbeatProcess"]', -}; -export const STAT_USER = { - value: '789', - domId: '[data-test-subj="host-stat-auditbeatUser"]', -}; -export const STAT_WINLOGBEAT_SECURITY = { - value: '70', - domId: '[data-test-subj="host-stat-winlogbeatSecurity"]', -}; -export const STAT_WINLOGBEAT_MWSYSMON_OPERATIONAL = { - value: '30', - domId: '[data-test-subj="host-stat-winlogbeatMWSysmonOperational"]', -}; - -export const HOST_STATS = [ - STAT_AUDITD, - ENDGAME_DNS, - ENDGAME_FILE, - ENDGAME_IMAGE_LOAD, - ENDGAME_NETWORK, - ENDGAME_PROCESS, - ENDGAME_REGISTRY, - ENDGAME_SECURITY, - STAT_FILEBEAT, - STAT_FIM, - STAT_LOGIN, - STAT_PACKAGE, - STAT_PROCESS, - STAT_USER, - STAT_WINLOGBEAT_SECURITY, - STAT_WINLOGBEAT_MWSYSMON_OPERATIONAL, -]; - -// Network Stats -export const STAT_SOCKET = { - value: '578,502', - domId: '[data-test-subj="network-stat-auditbeatSocket"]', -}; -export const STAT_CISCO = { - value: '999', - domId: '[data-test-subj="network-stat-filebeatCisco"]', -}; -export const STAT_NETFLOW = { - value: '2,544', - domId: '[data-test-subj="network-stat-filebeatNetflow"]', -}; -export const STAT_PANW = { - value: '678', - domId: '[data-test-subj="network-stat-filebeatPanw"]', -}; -export const STAT_SURICATA = { - value: '303,699', - domId: '[data-test-subj="network-stat-filebeatSuricata"]', -}; -export const STAT_ZEEK = { - value: '71,129', - domId: '[data-test-subj="network-stat-filebeatZeek"]', -}; -export const STAT_DNS = { - value: '1,090', - domId: '[data-test-subj="network-stat-packetbeatDNS"]', -}; -export const STAT_FLOW = { - value: '722,153', - domId: '[data-test-subj="network-stat-packetbeatFlow"]', -}; -export const STAT_TLS = { - value: '340', - domId: '[data-test-subj="network-stat-packetbeatTLS"]', -}; - -export const NETWORK_STATS = [ - STAT_SOCKET, - STAT_CISCO, - STAT_NETFLOW, - STAT_PANW, - STAT_SURICATA, - STAT_ZEEK, - STAT_DNS, - STAT_FLOW, - STAT_TLS, -]; - -export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; - -export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/pagination/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/pagination/selectors.ts deleted file mode 100644 index 8e05f3e54568e..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/pagination/selectors.ts +++ /dev/null @@ -1,14 +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. - */ - -export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-authentications-loading-false"]'; -export const getDraggableField = (field: string) => `[data-test-subj="draggable-content-${field}"]`; -export const getPageButtonSelector = (num: number) => `[data-test-subj="pagination-button-${num}"]`; -export const NAVIGATION_AUTHENTICATIONS = '[data-test-subj="navigation-authentications"]'; -export const NAVIGATION_UNCOMMON_PROCESSES = '[data-test-subj="navigation-uncommonProcesses"]'; -export const NUMBERED_PAGINATION = '[data-test-subj="numberedPagination"]'; -export const SUPER_DATE_PICKER_APPLY_BUTTON = '[data-test-subj="superDatePickerApplyTimeButton"]'; -export const UNCOMMON_PROCCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts deleted file mode 100644 index ef2c19bd7e737..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts +++ /dev/null @@ -1,59 +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 { drag, drop } from '../drag_n_drop/helpers'; -import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../hosts/selectors'; -import { - CLOSE_TIMELINE_BTN, - CREATE_NEW_TIMELINE, - SEARCH_OR_FILTER_CONTAINER, - SERVER_SIDE_EVENT_COUNT, - TIMELINE_DATA_PROVIDERS, - TIMELINE_SETTINGS, - TIMELINE_TOGGLE_BUTTON, - TOGGLE_TIMELINE_EXPAND_EVENT, -} from './selectors'; -import { DEFAULT_TIMEOUT } from '../util/helpers'; - -/** Toggles the timeline's open / closed state by clicking the `T I M E L I N E` button */ -export const toggleTimelineVisibility = () => - cy.get(TIMELINE_TOGGLE_BUTTON, { timeout: DEFAULT_TIMEOUT }).click(); - -export const createNewTimeline = () => { - cy.get(TIMELINE_SETTINGS).click(); - cy.get(CREATE_NEW_TIMELINE).click(); - cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); -}; - -/** Drags and drops a host from the `All Hosts` widget on the `Hosts` page to the timeline */ -export const dragFromAllHostsToTimeline = () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea)); -}; - -/** Executes the specified KQL query in the timeline */ -export const executeKQL = (query: string) => { - cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`); -}; - -/** A sample KQL query that finds any documents where the `host.name` field exists */ -export const hostExistsQuery = 'host.name: *'; - -/** Asserts that at least one event matches the timeline's search criteria */ -export const assertAtLeastOneEventMatchesSearch = () => - cy - .get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) - .invoke('text') - .should('be.above', 0); - -/** Toggles open or closed the first event in the timeline */ -export const toggleFirstTimelineEventDetails = () => { - cy.get(TOGGLE_TIMELINE_EXPAND_EVENT, { timeout: DEFAULT_TIMEOUT }) - .first() - .click({ force: true }); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts deleted file mode 100644 index 5515c1f7d58e2..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts +++ /dev/null @@ -1,47 +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. - */ - -/** A data provider rendered in the timeline's data providers drop area */ -export const DATA_PROVIDER = '[data-test-subj="providerContainer"]'; - -export const TIMELINE_FIELDS_BUTTON = - '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; - -/** Data providers are dropped and rendered in this area of the timeline */ -export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; - -/** The empty data providers area when no data providers have been added to the timeline */ -export const TIMELINE_DATA_PROVIDERS_EMPTY = - '[data-test-subj="dataProviders"] [data-test-subj="empty"]'; - -/** Data providers that were dropped on a timeline */ -export const TIMELINE_DROPPED_DATA_PROVIDERS = `${TIMELINE_DATA_PROVIDERS} ${DATA_PROVIDER}`; - -/** The `Timeline ^` button that toggles visibility of the Timeline */ -export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]'; - -/** The flyout button shown when a data provider is not ready to be dropped on the timeline */ -export const TIMELINE_NOT_READY_TO_DROP_BUTTON = - '[data-test-subj="flyout-button-not-ready-to-drop"]'; - -/** Contains the KQL bar for searching or filtering in the timeline */ -export const SEARCH_OR_FILTER_CONTAINER = - '[data-test-subj="timeline-search-or-filter-search-container"]'; - -/** The total server-side count of the events matching the timeline's search criteria */ -export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; - -/** Expands or collapses an event in the timeline */ -export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; - -/** The body of the timeline flyout */ -export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; - -export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; - -export const TIMELINE_SETTINGS = '[data-test-subj="settings-gear"]'; - -export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts deleted file mode 100644 index fa754cd4b8db4..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts +++ /dev/null @@ -1,47 +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. - */ - -/* - * Be Careful here by using iso date and epoch date - * because the conversion might not what you expect - * for different timezone better to calculate - * them on the fly - */ - -export const ABSOLUTE_DATE_RANGE = { - url: - '/app/siem#/network/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', - - urlUnlinked: - '/app/siem#/network/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', - urlKqlNetworkNetwork: `/app/siem#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlNetworkHosts: `/app/siem#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/siem#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/siem#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlHost: - '/app/siem#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', - urlHostNew: - '/app/siem#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', -}; -export const DATE_PICKER_START_DATE_POPOVER_BUTTON = - 'div[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerstartDatePopoverButton"]'; -export const DATE_PICKER_END_DATE_POPOVER_BUTTON = - '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerendDatePopoverButton"]'; -export const DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE = - '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerstartDatePopoverButton"]'; -export const DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE = - '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerendDatePopoverButton"]'; -export const DATE_PICKER_ABSOLUTE_TAB = '[data-test-subj="superDatePickerAbsoluteTab"]'; -export const DATE_PICKER_APPLY_BUTTON = - '[data-test-subj="globalDatePicker"] button[data-test-subj="querySubmitButton"]'; -export const DATE_PICKER_APPLY_BUTTON_TIMELINE = - '[data-test-subj="timeline-properties"] button[data-test-subj="superDatePickerApplyTimeButton"]'; -export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; -export const KQL_INPUT = '[data-test-subj="queryInput"]'; -export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; - -export const HOST_DETAIL_SIEM_KIBANA = '[data-test-subj="table-allHosts-loading-false"] a.euiLink'; -export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts deleted file mode 100644 index 18276580289c7..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts +++ /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. - */ - -/** The SIEM app's Hosts page */ -export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; -export const HOSTS_PAGE_TAB_URLS = { - allHosts: '/app/siem#/hosts/allHosts', - anomalies: '/app/siem#/hosts/anomalies', - authentications: '/app/siem#/hosts/authentications', - events: '/app/siem#/hosts/events', - uncommonProcesses: '/app/siem#/hosts/uncommonProcesses', -}; - -/** Kibana's login page */ -export const LOGIN_PAGE = '/login'; - -/** The SIEM app's Network page */ -export const NETWORK_PAGE = '/app/siem#/network'; -export const NETWORK_TAB_URLS = { - dns: `${NETWORK_PAGE}/dns`, -}; - -/** The SIEM app's Overview page */ -export const OVERVIEW_PAGE = '/app/siem#/overview'; - -/** The SIEM app's Timelines page */ -export const TIMELINES_PAGE = '/app/siem#/timelines'; - -/** Visit this URL to logout of Kibana */ -export const LOGOUT = '/logout'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/util/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/util/helpers.ts deleted file mode 100644 index 4f96430bb381e..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/util/helpers.ts +++ /dev/null @@ -1,27 +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 { login } from '../login/helpers'; - -/** The default time in ms to wait for a Cypress command to complete */ -export const DEFAULT_TIMEOUT = 30 * 1000; - -/** - * Authenticates with Kibana, visits the specified `url`, and waits for the - * Kibana logo to be displayed before continuing - */ -export const loginAndWaitForPage = (url: string) => { - login(); - - cy.visit(`${Cypress.config().baseUrl}${url}`); - - cy.viewport('macbook-15'); - - cy.contains('a', 'SIEM', { timeout: DEFAULT_TIMEOUT }); -}; - -export const waitForTableLoad = (dataTestSubj: string) => - cy.get(dataTestSubj, { timeout: DEFAULT_TIMEOUT }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/ml_conditional_links.spec.ts similarity index 93% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/ml_conditional_links.spec.ts index fabd86cf51f91..b02ed1a5e4c94 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/ml_conditional_links.spec.ts @@ -4,28 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KQL_INPUT } from '../screens/siem_header'; + +import { loginAndWaitForPage } from '../tasks/login'; + import { - mlNetworkSingleIpNullKqlQuery, - mlNetworkSingleIpKqlQuery, - mlNetworkMultipleIpNullKqlQuery, - mlNetworkMultipleIpKqlQuery, - mlNetworkNullKqlQuery, - mlNetworkKqlQuery, - mlHostSingleHostNullKqlQuery, - mlHostSingleHostKqlQueryVariable, - mlHostSingleHostKqlQuery, - mlHostMultiHostNullKqlQuery, mlHostMultiHostKqlQuery, - mlHostVariableHostNullKqlQuery, + mlHostMultiHostNullKqlQuery, + mlHostSingleHostKqlQuery, + mlHostSingleHostKqlQueryVariable, + mlHostSingleHostNullKqlQuery, mlHostVariableHostKqlQuery, -} from '../../../urls/ml_conditional_links'; -import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../../tasks/login'; -import { KQL_INPUT } from '../../../screens/header'; + mlHostVariableHostNullKqlQuery, + mlNetworkKqlQuery, + mlNetworkMultipleIpKqlQuery, + mlNetworkMultipleIpNullKqlQuery, + mlNetworkNullKqlQuery, + mlNetworkSingleIpKqlQuery, + mlNetworkSingleIpNullKqlQuery, +} from '../urls/ml_conditional_links'; describe('ml conditional links', () => { it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -34,7 +36,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple IPs with a null for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))' @@ -43,7 +45,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple IPs with a value for the query', () => { loginAndWaitForPage(mlNetworkMultipleIpKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))' @@ -52,7 +54,7 @@ describe('ml conditional links', () => { it('sets the KQL from a $ip$ with a value for the query', () => { loginAndWaitForPage(mlNetworkKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -61,7 +63,7 @@ describe('ml conditional links', () => { it('sets the KQL from a single host name with a value for query', () => { loginAndWaitForPage(mlHostSingleHostKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' @@ -70,7 +72,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple host names with null for query', () => { loginAndWaitForPage(mlHostMultiHostNullKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(host.name: "siem-windows" or host.name: "siem-suricata")' @@ -79,7 +81,7 @@ describe('ml conditional links', () => { it('sets the KQL from a multiple host names with a value for query', () => { loginAndWaitForPage(mlHostMultiHostKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))' @@ -88,7 +90,7 @@ describe('ml conditional links', () => { it('sets the KQL from a undefined/null host name but with a value for query', () => { loginAndWaitForPage(mlHostVariableHostKqlQuery); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( + cy.get(KQL_INPUT).should( 'have.attr', 'value', '(process.name: "conhost.exe" or process.name: "sc.exe")' diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/navigation.spec.ts similarity index 78% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/navigation.spec.ts index 364864b395d41..2c5a0e5eeea8a 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/navigation/navigation.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/navigation.spec.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../screens/siem_header'; -import { TIMELINES_PAGE } from '../../../urls/navigation'; -import { HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../../../screens/header'; -import { loginAndWaitForPage } from '../../../tasks/login'; -import { navigateFromHeaderTo } from '../../../tasks/header'; +import { loginAndWaitForPage } from '../tasks/login'; +import { navigateFromHeaderTo } from '../tasks/siem_header'; + +import { TIMELINES_PAGE } from '../urls/navigation'; describe('top-level navigation common to all pages in the SIEM app', () => { before(() => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/overview.spec.ts similarity index 74% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/overview.spec.ts index 64002aadc86d8..cadb4beca0f9e 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/overview/overview.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/overview.spec.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OVERVIEW_PAGE } from '../../../urls/navigation'; -import { HOST_STATS, NETWORK_STATS } from '../../../screens/overview'; -import { expandHostStats, expandNetworkStats } from '../../../tasks/overview'; -import { loginAndWaitForPage } from '../../lib/util/helpers'; +import { HOST_STATS, NETWORK_STATS } from '../screens/overview'; + +import { expandHostStats, expandNetworkStats } from '../tasks/overview'; +import { loginAndWaitForPage } from '../tasks/login'; + +import { OVERVIEW_PAGE } from '../urls/navigation'; describe('Overview Page', () => { before(() => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/pagination.spec.ts similarity index 80% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/pagination.spec.ts index d8ad75322b889..482c97fe29c3b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/pagination.spec.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE_TAB_URLS } from '../../../urls/navigation'; -import { loginAndWaitForPage } from '../../../tasks/login'; -import { refreshPage } from '../../../tasks/header'; -import { goToFirstPage, goToThirdPage } from '../../../tasks/pagination'; -import { FIRST_PAGE_SELECTOR, THIRD_PAGE_SELECTOR } from '../../../screens/pagination'; -import { PROCESS_NAME_FIELD } from '../../../screens/uncommon_processes'; -import { waitForUncommonProcessesToBeLoaded } from '../../../tasks/uncommon_processes'; -import { waitForAuthenticationsToBeLoaded } from '../../../tasks/authentications'; -import { openAuthentications, openUncommonProcesses } from '../../../tasks/hosts/main'; +import { PROCESS_NAME_FIELD } from '../screens/hosts/uncommon_processes'; +import { FIRST_PAGE_SELECTOR, THIRD_PAGE_SELECTOR } from '../screens/pagination'; + +import { waitForAuthenticationsToBeLoaded } from '../tasks/hosts/authentications'; +import { openAuthentications, openUncommonProcesses } from '../tasks/hosts/main'; +import { waitForUncommonProcessesToBeLoaded } from '../tasks/hosts/uncommon_processes'; +import { loginAndWaitForPage } from '../tasks/login'; +import { goToFirstPage, goToThirdPage } from '../tasks/pagination'; +import { refreshPage } from '../tasks/siem_header'; + +import { HOSTS_PAGE_TAB_URLS } from '../urls/navigation'; describe('Pagination', () => { before(() => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts similarity index 81% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts index 7d1ee43b1b509..4889d40ae7d39 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts @@ -4,22 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE } from '../../../urls/navigation'; import { - waitForAllHostsToBeLoaded, - dragAndDropFirstHostToTimeline, - dragFirstHostToTimeline, - dragFirstHostToEmptyTimelineDataProviders, -} from '../../../tasks/hosts/all_hosts'; -import { HOSTS_NAMES_DRAGGABLE } from '../../../screens/hosts/all_hosts'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; -import { createNewTimeline } from '../../../tasks/timeline/main'; -import { openTimeline } from '../../../tasks/siem_main'; -import { - TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS, + TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DROPPED_DATA_PROVIDERS, -} from '../../../screens/timeline/main'; +} from '../screens/timeline'; +import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts'; + +import { + dragAndDropFirstHostToTimeline, + dragFirstHostToEmptyTimelineDataProviders, + dragFirstHostToTimeline, + waitForAllHostsToBeLoaded, +} from '../tasks/hosts/all_hosts'; + +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline } from '../tasks/siem_main'; +import { createNewTimeline } from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; describe('timeline data providers', () => { before(() => { @@ -38,7 +41,7 @@ describe('timeline data providers', () => { it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { dragAndDropFirstHostToTimeline(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { timeout: DEFAULT_TIMEOUT }) + cy.get(TIMELINE_DROPPED_DATA_PROVIDERS) .first() .invoke('text') .then(dataProviderText => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts similarity index 68% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts index b7faaaac1c06c..1a94a4abbe5bf 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE } from '../../../urls/navigation'; -import { waitForAllHostsToBeLoaded, dragFirstHostToTimeline } from '../../../tasks/hosts/all_hosts'; -import { loginAndWaitForPage } from '../../../tasks/login'; -import { openTimelineIfClosed, openTimeline } from '../../../tasks/siem_main'; -import { - TIMELINE_FLYOUT_BODY, - TIMELINE_NOT_READY_TO_DROP_BUTTON, -} from '../../../screens/timeline/main'; -import { createNewTimeline } from '../../../tasks/timeline/main'; +import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; + +import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline, openTimelineIfClosed } from '../tasks/siem_main'; +import { createNewTimeline } from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; describe('timeline flyout button', () => { before(() => { diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts similarity index 58% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts index 28cc4a6e8827d..c06fd69a558a4 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/search_or_filter.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SERVER_SIDE_EVENT_COUNT } from '../../../screens/timeline/main'; -import { HOSTS_PAGE } from '../../../urls/navigation'; -import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../../tasks/login'; -import { openTimeline } from '../../../tasks/siem_main'; -import { executeTimelineKQL } from '../../../tasks/timeline/main'; +import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; + +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline } from '../tasks/siem_main'; +import { executeTimelineKQL } from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; describe('timeline search or filter KQL bar', () => { beforeEach(() => { @@ -20,7 +22,7 @@ describe('timeline search or filter KQL bar', () => { openTimeline(); executeTimelineKQL(hostExistsQuery); - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) + cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') .should('be.above', 0); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts similarity index 82% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts index ccd6ae818c6cd..7b2c6f3b55b2e 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_toggle_column.spec.ts @@ -4,23 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE } from '../../../urls/navigation'; -import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../../tasks/login'; import { + ID_HEADER_FIELD, + ID_TOGGLE_FIELD, + TIMESTAMP_HEADER_FIELD, + TIMESTAMP_TOGGLE_FIELD, +} from '../screens/timeline'; + +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimeline } from '../tasks/siem_main'; +import { + checkIdToggleField, createNewTimeline, - populateTimeline, + dragAndDropIdToggleFieldToTimeline, expandFirstTimelineEventDetails, + populateTimeline, uncheckTimestampToggleField, - checkIdToggleField, - dragAndDropIdToggleFieldToTimeline, -} from '../../../tasks/timeline/main'; -import { openTimeline } from '../../../tasks/siem_main'; -import { - TIMESTAMP_TOGGLE_FIELD, - ID_TOGGLE_FIELD, - TIMESTAMP_HEADER_FIELD, - ID_HEADER_FIELD, -} from '../../../screens/timeline/main'; +} from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; describe('toggle column in timeline', () => { before(() => { @@ -63,8 +65,6 @@ describe('toggle column in timeline', () => { expandFirstTimelineEventDetails(); dragAndDropIdToggleFieldToTimeline(); - cy.get(ID_HEADER_FIELD, { - timeout: DEFAULT_TIMEOUT, - }).should('exist'); + cy.get(ID_HEADER_FIELD).should('exist'); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts similarity index 84% rename from x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts rename to x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts index 4345938c8867e..cabdb98fa5b67 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ABSOLUTE_DATE_RANGE } from '../../../urls/state'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; -import { HOSTS_PAGE } from '../../../urls/navigation'; +import { + DATE_PICKER_END_DATE_POPOVER_BUTTON, + DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE, + DATE_PICKER_START_DATE_POPOVER_BUTTON, + DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, +} from '../screens/date_picker'; +import { HOSTS_NAMES } from '../screens/hosts/all_hosts'; +import { ANOMALIES_TAB } from '../screens/hosts/main'; +import { BREADCRUMBS, HOSTS, KQL_INPUT, NETWORK } from '../screens/siem_header'; +import { SERVER_SIDE_EVENT_COUNT, TIMELINE_TITLE } from '../screens/timeline'; + +import { loginAndWaitForPage } from '../tasks/login'; import { setStartDate, setEndDate, @@ -14,23 +23,17 @@ import { setTimelineStartDate, setTimelineEndDate, updateTimelineDates, -} from '../../../tasks/calendar'; -import { waitForIpsTableToBeLoaded } from '../../../tasks/network/flows'; -import { openTimeline } from '../../../tasks/siem_main'; -import { - DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, - DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE, - DATE_PICKER_START_DATE_POPOVER_BUTTON, - DATE_PICKER_END_DATE_POPOVER_BUTTON, -} from '../../../screens/calendar'; -import { kqlSearch, navigateFromHeaderTo, clearSearchBar } from '../../../tasks/header'; -import { HOSTS, NETWORK, KQL_INPUT, BREADCRUMBS } from '../../../screens/header'; -import { openAllHosts } from '../../../tasks/hosts/main'; -import { ANOMALIES_TAB } from '../../../screens/hosts/main'; -import { waitForAllHostsToBeLoaded, openFirstHostDetails } from '../../../tasks/hosts/all_hosts'; -import { HOSTS_NAMES } from '../../../screens/hosts/all_hosts'; -import { executeTimelineKQL, addNameToTimeline } from '../../../tasks/timeline/main'; -import { SERVER_SIDE_EVENT_COUNT, TIMELINE_TITLE } from '../../../screens/timeline/main'; +} from '../tasks/date_picker'; +import { openFirstHostDetails, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; +import { openAllHosts } from '../tasks/hosts/main'; + +import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; +import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/siem_header'; +import { openTimeline } from '../tasks/siem_main'; +import { addNameToTimeline, executeTimelineKQL } from '../tasks/timeline'; + +import { HOSTS_PAGE } from '../urls/navigation'; +import { ABSOLUTE_DATE_RANGE } from '../urls/state'; const ABSOLUTE_DATE = { endTime: '1564691609186', @@ -146,20 +149,12 @@ describe('url state', () => { it('sets kql on network page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlNetworkNetwork); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( - 'have.attr', - 'value', - 'source.ip: "10.142.0.9"' - ); + cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); it('sets kql on hosts page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( - 'have.attr', - 'value', - 'source.ip: "10.142.0.9"' - ); + cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); it('sets the url state when kql is set', () => { @@ -197,7 +192,7 @@ describe('url state', () => { 'href', "#/link-to/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); - cy.get(HOSTS_NAMES, { timeout: DEFAULT_TIMEOUT }) + cy.get(HOSTS_NAMES) .first() .invoke('text') .should('eq', 'siem-kibana'); @@ -231,11 +226,7 @@ describe('url state', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); navigateFromHeaderTo(NETWORK); - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).should( - 'have.attr', - 'value', - 'source.ip: "10.142.0.9"' - ); + cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); it('sets and reads the url state for timeline by id', () => { @@ -243,7 +234,7 @@ describe('url state', () => { openTimeline(); executeTimelineKQL('host.name: *'); - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) + cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') .should('be.above', 0); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/calendar.ts b/x-pack/legacy/plugins/siem/cypress/screens/date_picker.ts similarity index 99% rename from x-pack/legacy/plugins/siem/cypress/screens/calendar.ts rename to x-pack/legacy/plugins/siem/cypress/screens/date_picker.ts index caff721c683e9..e49f5afa7bd0c 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/calendar.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/date_picker.ts @@ -4,18 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -export const DATE_PICKER_START_DATE_POPOVER_BUTTON = - 'div[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerstartDatePopoverButton"]'; -export const DATE_PICKER_END_DATE_POPOVER_BUTTON = - '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerendDatePopoverButton"]'; -export const DATE_PICKER_ABSOLUTE_TAB = '[data-test-subj="superDatePickerAbsoluteTab"]'; +export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; + export const DATE_PICKER_APPLY_BUTTON = '[data-test-subj="globalDatePicker"] button[data-test-subj="querySubmitButton"]'; + export const DATE_PICKER_APPLY_BUTTON_TIMELINE = '[data-test-subj="timeline-properties"] button[data-test-subj="superDatePickerApplyTimeButton"]'; -export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; -export const DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE = - '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerstartDatePopoverButton"]'; + +export const DATE_PICKER_ABSOLUTE_TAB = '[data-test-subj="superDatePickerAbsoluteTab"]'; + +export const DATE_PICKER_END_DATE_POPOVER_BUTTON = + '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerendDatePopoverButton"]'; export const DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE = '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerendDatePopoverButton"]'; + +export const DATE_PICKER_START_DATE_POPOVER_BUTTON = + 'div[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerstartDatePopoverButton"]'; + +export const DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE = + '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerstartDatePopoverButton"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/fields_browser.ts b/x-pack/legacy/plugins/siem/cypress/screens/fields_browser.ts similarity index 92% rename from x-pack/legacy/plugins/siem/cypress/screens/timeline/fields_browser.ts rename to x-pack/legacy/plugins/siem/cypress/screens/fields_browser.ts index aa63aaf89f98b..f15096bd874bc 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/fields_browser.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/fields_browser.ts @@ -4,57 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]'; +export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; -/** Typing in this input filters the Field Browser */ -export const FIELDS_BROWSER_FILTER_INPUT = '[data-test-subj="field-search"]'; +export const FIELDS_BROWSER_CHECKBOX = (id: string) => { + return `[data-test-subj="field-${id}-checkbox`; +}; -/** The title of the selected category in the right-hand side of the fields browser */ -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]'; +export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = - '[data-test-subj="selected-category-count-badge"]'; +export const FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER = + '[data-test-subj="timeline"] [data-test-subj="field-name-host.geo.country_name"]'; -export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; +export const FIELDS_BROWSER_FIELDS_COUNT = '[data-test-subj="fields-count"]'; -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = '[data-test-subj="host-category-count"]'; +export const FIELDS_BROWSER_FILTER_INPUT = '[data-test-subj="field-search"]'; -export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = '[data-test-subj="system-category-count"]'; +export const FIELDS_BROWSER_HEADER_DROP_AREA = + '[data-test-subj="timeline"] [data-test-subj="headers-group"]'; -export const FIELDS_BROWSER_FIELDS_COUNT = '[data-test-subj="fields-count"]'; +export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = '[data-test-subj="host-category-count"]'; -/** Contains the body of the fields browser */ -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; +export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = + '[data-test-subj="field-host.geo.city_name-checkbox"]'; -export const FIELDS_BROWSER_MESSAGE_HEADER = - '[data-test-subj="timeline"] [data-test-subj="header-text-message"]'; +export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = + '[data-test-subj="header-text-host.geo.city_name"]'; -export const FIELDS_BROWSER_MESSAGE_CHECKBOX = - '[data-test-subj="timeline"] [data-test-subj="field-message-checkbox"]'; +export const FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX = + '[data-test-subj="field-host.geo.continent_name-checkbox"]'; -export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER = - '[data-test-subj="header-text-host.geo.country_name"]'; +export const FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER = + '[data-test-subj="header-text-host.geo.continent_name"]'; export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_CHECKBOX = '[data-test-subj="field-host.geo.country_name-checkbox"]'; -export const FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER = - '[data-test-subj="timeline"] [data-test-subj="field-name-host.geo.country_name"]'; +export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER = + '[data-test-subj="header-text-host.geo.country_name"]'; -export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = - '[data-test-subj="header-text-host.geo.city_name"]'; +export const FIELDS_BROWSER_MESSAGE_CHECKBOX = + '[data-test-subj="timeline"] [data-test-subj="field-message-checkbox"]'; -export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.city_name-checkbox"]'; +export const FIELDS_BROWSER_MESSAGE_HEADER = + '[data-test-subj="timeline"] [data-test-subj="header-text-message"]'; -export const FIELDS_BROWSER_HEADER_DROP_AREA = - '[data-test-subj="timeline"] [data-test-subj="headers-group"]'; +export const FIELDS_BROWSER_RESET_FIELDS = + '[data-test-subj="timeline"] [data-test-subj="reset-fields"]'; -export const FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER = - '[data-test-subj="header-text-host.geo.continent_name"]'; +export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]'; -export const FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.continent_name-checkbox"]'; +export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = + '[data-test-subj="selected-category-count-badge"]'; -export const FIELDS_BROWSER_RESET_FIELDS = - '[data-test-subj="timeline"] [data-test-subj="reset-fields"]'; +export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]'; + +export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = '[data-test-subj="system-category-count"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts index 61f39ca7a8b0c..ca123a8afc6c9 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts @@ -6,6 +6,6 @@ export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; -export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="draggable-content-host.name"]'; - export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"] a.euiLink'; + +export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="draggable-content-host.name"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/authentications.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/authentications.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/authentications.ts rename to x-pack/legacy/plugins/siem/cypress/screens/hosts/authentications.ts diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts index 034c1453fc979..ed46a90c872c8 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/events.ts @@ -4,33 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]'; - export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; -export const HEADER_SUBTITLE = `${EVENTS_VIEWER_PANEL} [data-test-subj="header-panel-subtitle"]`; +export const EVENTS_VIEWER_FIELDS_BUTTON = + '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser-gear"]'; -export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; - -export const INSPECT_QUERY = `${EVENTS_VIEWER_PANEL} [data-test-subj="inspect-icon-button"]`; +export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]'; -export const LOAD_MORE = `${EVENTS_VIEWER_PANEL} [data-test-subj="TimelineMoreButton"]`; +export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; -export const LOCAL_EVENTS_COUNT = `${EVENTS_VIEWER_PANEL} [data-test-subj="local-events-count"]`; +export const HEADER_SUBTITLE = + '[data-test-subj="events-viewer-panel"] [data-test-subj="header-panel-subtitle"]'; -export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const HOST_GEO_CITY_NAME_CHECKBOX = '[data-test-subj="field-host.geo.city_name-checkbox"]'; export const HOST_GEO_CITY_NAME_HEADER = '[data-test-subj="header-text-host.geo.city_name"]'; -export const HOST_GEO_CITY_NAME_CHECKBOX = '[data-test-subj="field-host.geo.city_name-checkbox"]'; +export const HOST_GEO_COUNTRY_NAME_CHECKBOX = + '[data-test-subj="field-host.geo.country_name-checkbox"]'; export const HOST_GEO_COUNTRY_NAME_HEADER = '[data-test-subj="header-text-host.geo.country_name"]'; -export const HOST_GEO_COUNTRY_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.country_name-checkbox"]'; +export const INSPECT_MODAL = '[data-test-subj="modal-inspect-euiModal"]'; -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; +export const INSPECT_QUERY = + '[data-test-subj="events-viewer-panel"] [data-test-subj="inspect-icon-button"]'; + +export const LOCAL_EVENTS_COUNT = + '[data-test-subj="events-viewer-panel"] [data-test-subj="local-events-count"'; -export const EVENTS_VIEWER_FIELDS_BUTTON = `${EVENTS_VIEWER_PANEL} [data-test-subj="show-field-browser-gear"]`; +export const LOAD_MORE = + '[data-test-subj="events-viewer-panel"] [data-test-subj="TimelineMoreButton"'; -export const RESET_FIELDS = `${EVENTS_VIEWER_PANEL} [data-test-subj="reset-fields"]`; +export const RESET_FIELDS = + '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; + +export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/fields_browser.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/fields_browser.ts deleted file mode 100644 index 252fa7d44a7c7..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/fields_browser.ts +++ /dev/null @@ -1,20 +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. - */ - -/** Clicking this button in the timeline opens the Fields browser */ - -/** The title displayed in the fields browser (i.e. Customize Columns) */ -export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]'; - -/** Contains the body of the fields browser */ -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; - -/** The title of the selected category in the right-hand side of the fields browser */ -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]'; - -export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="field-${id}-checkbox`; -}; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts index 25696be526e5f..3c30562680e86 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export const EVENTS_TAB = '[data-test-subj="navigation-events"]'; - -export const AUTHENTICATIONS_TAB = '[data-test-subj="navigation-authentications"]'; - -export const UNCOMMON_PROCESSES_TAB = '[data-test-subj="navigation-uncommonProcesses"]'; - export const ALL_HOSTS_TAB = '[data-test-subj="navigation-allHosts'; export const ANOMALIES_TAB = '[data-test-subj="navigation-anomalies"]'; +export const AUTHENTICATIONS_TAB = '[data-test-subj="navigation-authentications"]'; + +export const EVENTS_TAB = '[data-test-subj="navigation-events"]'; + export const KQL_SEARCH_BAR = '[data-test-subj="queryInput"]'; + +export const UNCOMMON_PROCESSES_TAB = '[data-test-subj="navigation-uncommonProcesses"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts index 9e15bea79eae0..68ea8e008bfd3 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PROCESS_NAME = '[data-test-subj="draggable-content-process.name"]'; +export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]'; + +export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/header.ts b/x-pack/legacy/plugins/siem/cypress/screens/siem_header.ts similarity index 100% rename from x-pack/legacy/plugins/siem/cypress/screens/header.ts rename to x-pack/legacy/plugins/siem/cypress/screens/siem_header.ts index 4ffb497a62432..cf1059269393a 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/header.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/siem_header.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export const KQL_INPUT = '[data-test-subj="queryInput"]'; +export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; export const HOSTS = '[data-test-subj="navigation-hosts"]'; +export const KQL_INPUT = '[data-test-subj="queryInput"]'; + export const NETWORK = '[data-test-subj="navigation-network"]'; export const OVERVIEW = '[data-test-subj="navigation-overview"]'; -export const TIMELINES = '[data-test-subj="navigation-timelines"]'; - export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]'; -export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; +export const TIMELINES = '[data-test-subj="navigation-timelines"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts similarity index 89% rename from x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts rename to x-pack/legacy/plugins/siem/cypress/screens/timeline.ts index 6df269b7691a8..1640647b45427 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts @@ -4,26 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -/** The `Timeline ^` button that toggles visibility of the Timeline */ -export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]'; - -/** Contains the KQL bar for searching or filtering in the timeline */ -export const SEARCH_OR_FILTER_CONTAINER = - '[data-test-subj="timeline-search-or-filter-search-container"]'; +export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; -export const TIMELINE_FIELDS_BUTTON = - '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; -/** The total server-side count of the events matching the timeline's search criteria */ -export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const ID_HEADER_FIELD = '[data-test-subj="timeline"] [data-test-subj="header-text-_id"]'; -export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; +export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name-_id"]'; -export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; +export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; -export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; +export const SEARCH_OR_FILTER_CONTAINER = + '[data-test-subj="timeline-search-or-filter-search-container"]'; -export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; +export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -33,21 +27,24 @@ export const TIMELINE_DATA_PROVIDERS_EMPTY = export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="dataProviders"] [data-test-subj="providerContainer"]'; +export const TIMELINE_FIELDS_BUTTON = + '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; + export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; +export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; + export const TIMELINE_NOT_READY_TO_DROP_BUTTON = '[data-test-subj="flyout-button-not-ready-to-drop"]'; -export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; - -export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; +export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; -export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; +export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; -export const ID_HEADER_FIELD = '[data-test-subj="timeline"] [data-test-subj="header-text-_id"]'; +export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]'; -export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name-_id"]'; +export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; -export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; +export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts index 39a61401c15b3..e02d3506b33bc 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts @@ -33,6 +33,13 @@ export const drag = (subject: JQuery) => { .wait(1); }; +/** Drags the subject being dragged on the specified drop target, but does not drop it */ +export const dragWithoutDrop = (dropTarget: JQuery) => { + cy.wrap(dropTarget).trigger('mousemove', 'center', { + button: primaryButton, + }); +}; + /** "Drops" the subject being dragged on the specified drop target */ export const drop = (dropTarget: JQuery) => { cy.wrap(dropTarget) @@ -41,10 +48,3 @@ export const drop = (dropTarget: JQuery) => { .trigger('mouseup', { force: true }) .wait(1); }; - -/** Drags the subject being dragged on the specified drop target, but does not drop it */ -export const dragWithoutDrop = (dropTarget: JQuery) => { - cy.wrap(dropTarget).trigger('mousemove', 'center', { - button: primaryButton, - }); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/calendar.ts b/x-pack/legacy/plugins/siem/cypress/tasks/date_picker.ts similarity index 70% rename from x-pack/legacy/plugins/siem/cypress/tasks/calendar.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/date_picker.ts index 16231317d6aef..9d79b73a52b08 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/calendar.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/date_picker.ts @@ -5,73 +5,67 @@ */ import { - DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, - DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE, - DATE_PICKER_APPLY_BUTTON_TIMELINE, - DATE_PICKER_START_DATE_POPOVER_BUTTON, DATE_PICKER_ABSOLUTE_TAB, DATE_PICKER_ABSOLUTE_INPUT, DATE_PICKER_APPLY_BUTTON, + DATE_PICKER_APPLY_BUTTON_TIMELINE, DATE_PICKER_END_DATE_POPOVER_BUTTON, -} from '../screens/calendar'; - -import { DEFAULT_TIMEOUT } from '../tasks/login'; + DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE, + DATE_PICKER_START_DATE_POPOVER_BUTTON, + DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, +} from '../screens/date_picker'; -export const setStartDate = (date: string) => { - cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).click({ force: true }); +export const setEndDate = (date: string) => { + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_TAB) .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }) + cy.get(DATE_PICKER_ABSOLUTE_INPUT) .clear() .type(date); }; -export const setEndDate = (date: string) => { - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).click({ force: true }); +export const setStartDate = (date: string) => { + cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_TAB) .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }) + cy.get(DATE_PICKER_ABSOLUTE_INPUT) .clear() .type(date); }; -export const updateDates = () => { - cy.get(DATE_PICKER_APPLY_BUTTON, { timeout: DEFAULT_TIMEOUT }) - .click({ force: true }) - .invoke('text', { timeout: DEFAULT_TIMEOUT }) - .should('not.equal', 'Updating'); -}; - -export const setTimelineStartDate = (date: string) => { - cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, { timeout: DEFAULT_TIMEOUT }).click({ - force: true, - }); +export const setTimelineEndDate = (date: string) => { + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_TAB) .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }).type( - `{selectall}{backspace}${date}{enter}` - ); + cy.get(DATE_PICKER_ABSOLUTE_INPUT).type(`{selectall}{backspace}${date}{enter}`); }; -export const setTimelineEndDate = (date: string) => { - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).click({ force: true }); +export const setTimelineStartDate = (date: string) => { + cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).click({ + force: true, + }); cy.get(DATE_PICKER_ABSOLUTE_TAB) .first() .click({ force: true }); - cy.get(DATE_PICKER_ABSOLUTE_INPUT, { timeout: DEFAULT_TIMEOUT }).type( - `{selectall}{backspace}${date}{enter}` - ); + cy.get(DATE_PICKER_ABSOLUTE_INPUT).type(`{selectall}{backspace}${date}{enter}`); +}; + +export const updateDates = () => { + cy.get(DATE_PICKER_APPLY_BUTTON) + .click({ force: true }) + .invoke('text') + .should('not.equal', 'Updating'); }; export const updateTimelineDates = () => { diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts b/x-pack/legacy/plugins/siem/cypress/tasks/fields_browser.ts similarity index 81% rename from x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/fields_browser.ts index d30e49a25bab0..e1d2f24da424c 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/fields_browser.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/fields_browser.ts @@ -3,42 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { drag, drop } from '../../integration/lib/drag_n_drop/helpers'; +import { drag, drop } from '../tasks/common'; import { FIELDS_BROWSER_FILTER_INPUT, - FIELDS_BROWSER_MESSAGE_CHECKBOX, - FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX, FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER, FIELDS_BROWSER_HEADER_DROP_AREA, + FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX, FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX, + FIELDS_BROWSER_MESSAGE_CHECKBOX, FIELDS_BROWSER_RESET_FIELDS, -} from '../../screens/timeline/fields_browser'; -import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; -import { KQL_SEARCH_BAR } from '../../screens/hosts/main'; - -export const clearFieldsBrowser = () => { - cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); -}; - -export const filterFieldsBrowser = (fieldName: string) => { - cy.get(FIELDS_BROWSER_FILTER_INPUT, { timeout: DEFAULT_TIMEOUT }) - .type(fieldName) - .should('not.have.class', 'euiFieldSearch-isLoading'); -}; - -export const closeFieldsBrowser = () => { - cy.get(KQL_SEARCH_BAR, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); -}; +} from '../screens/fields_browser'; +import { KQL_SEARCH_BAR } from '../screens/hosts/main'; -export const removesMessageField = () => { - cy.get(FIELDS_BROWSER_MESSAGE_CHECKBOX).uncheck({ +export const addsHostGeoCityNameToTimeline = () => { + cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX).check({ force: true, }); }; -export const addsHostGeoCityNameToTimeline = () => { - cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX).check({ +export const addsHostGeoContinentNameToTimeline = () => { + cy.get(FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX).check({ force: true, }); }; @@ -50,8 +35,22 @@ export const addsHostGeoCountryNameToTimelineDraggingIt = () => { cy.get(FIELDS_BROWSER_HEADER_DROP_AREA).then(headersDropArea => drop(headersDropArea)); }; -export const addsHostGeoContinentNameToTimeline = () => { - cy.get(FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX).check({ +export const clearFieldsBrowser = () => { + cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); +}; + +export const closeFieldsBrowser = () => { + cy.get(KQL_SEARCH_BAR).click({ force: true }); +}; + +export const filterFieldsBrowser = (fieldName: string) => { + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .type(fieldName) + .should('not.have.class', 'euiFieldSearch-isLoading'); +}; + +export const removesMessageField = () => { + cy.get(FIELDS_BROWSER_MESSAGE_CHECKBOX).uncheck({ force: true, }); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts index 7146c132db4a0..312df96df1ddf 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts @@ -5,16 +5,9 @@ */ import { ALL_HOSTS_TABLE, HOSTS_NAMES_DRAGGABLE, HOSTS_NAMES } from '../../screens/hosts/all_hosts'; -import { - TIMELINE_DATA_PROVIDERS, - TIMELINE_DATA_PROVIDERS_EMPTY, -} from '../../screens/timeline/main'; -import { DEFAULT_TIMEOUT } from '../../tasks/login'; -import { drag, drop, dragWithoutDrop } from '../../tasks/common'; +import { TIMELINE_DATA_PROVIDERS, TIMELINE_DATA_PROVIDERS_EMPTY } from '../../screens/timeline'; -export const waitForAllHostsToBeLoaded = () => { - cy.get(ALL_HOSTS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); -}; +import { drag, dragWithoutDrop, drop } from '../../tasks/common'; export const dragAndDropFirstHostToTimeline = () => { cy.get(HOSTS_NAMES_DRAGGABLE) @@ -23,12 +16,6 @@ export const dragAndDropFirstHostToTimeline = () => { cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea)); }; -export const dragFirstHostToTimeline = () => { - cy.get(HOSTS_NAMES_DRAGGABLE) - .first() - .then(host => drag(host)); -}; - export const dragFirstHostToEmptyTimelineDataProviders = () => { cy.get(HOSTS_NAMES_DRAGGABLE) .first() @@ -39,8 +26,17 @@ export const dragFirstHostToEmptyTimelineDataProviders = () => { ); }; +export const dragFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES_DRAGGABLE) + .first() + .then(host => drag(host)); +}; export const openFirstHostDetails = () => { cy.get(HOSTS_NAMES) .first() .click({ force: true }); }; + +export const waitForAllHostsToBeLoaded = () => { + cy.get(ALL_HOSTS_TABLE).should('exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts similarity index 60% rename from x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts index 6fa4bf72ca2b2..f5f15150e8ac3 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AUTHENTICATIONS_TABLE } from '../screens/authentications'; -import { DEFAULT_TIMEOUT } from '../tasks/login'; +import { AUTHENTICATIONS_TABLE } from '../../screens/hosts/authentications'; export const waitForAuthenticationsToBeLoaded = () => { - cy.get(AUTHENTICATIONS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); + cy.get(AUTHENTICATIONS_TABLE).should('exist'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts index d84d62f82d2e6..de0c99bd31dff 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/events.ts @@ -4,37 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; import { - EVENTS_VIEWER_FIELDS_BUTTON, CLOSE_MODAL, - INSPECT_QUERY, - SERVER_SIDE_EVENT_COUNT, + EVENTS_VIEWER_FIELDS_BUTTON, + FIELDS_BROWSER_CONTAINER, HOST_GEO_CITY_NAME_CHECKBOX, HOST_GEO_COUNTRY_NAME_CHECKBOX, - FIELDS_BROWSER_CONTAINER, - RESET_FIELDS, + INSPECT_QUERY, LOAD_MORE, + RESET_FIELDS, + SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; -export const closeModal = () => { - cy.get(CLOSE_MODAL, { timeout: DEFAULT_TIMEOUT }).click(); -}; - -export const opensInspectQueryModal = () => { - cy.get(INSPECT_QUERY, { timeout: DEFAULT_TIMEOUT }) - .should('exist') - .trigger('mousemove', { force: true }) - .click({ force: true }); -}; - -export const waitsForEventsToBeLoaded = () => { - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) - .should('exist') - .invoke('text', { timeout: DEFAULT_TIMEOUT }) - .should('not.equal', '0'); -}; - export const addsHostGeoCityNameToHeader = () => { cy.get(HOST_GEO_CITY_NAME_CHECKBOX).check({ force: true, @@ -47,20 +28,38 @@ export const addsHostGeoCountryNameToHeader = () => { }); }; -export const resetFields = () => { - cy.get(RESET_FIELDS).click({ force: true }); +export const closeModal = () => { + cy.get(CLOSE_MODAL).click(); +}; + +export const loadMoreEvents = () => { + cy.get(LOAD_MORE).click({ force: true }); }; export const openEventsViewerFieldsBrowser = () => { - cy.get(EVENTS_VIEWER_FIELDS_BUTTON, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); + cy.get(EVENTS_VIEWER_FIELDS_BUTTON).click({ force: true }); - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) + cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') .should('not.equal', '0'); cy.get(FIELDS_BROWSER_CONTAINER).should('exist'); }; -export const loadMoreEvents = () => { - cy.get(LOAD_MORE).click({ force: true }); +export const opensInspectQueryModal = () => { + cy.get(INSPECT_QUERY) + .should('exist') + .trigger('mousemove', { force: true }) + .click({ force: true }); +}; + +export const resetFields = () => { + cy.get(RESET_FIELDS).click({ force: true }); +}; + +export const waitsForEventsToBeLoaded = () => { + cy.get(SERVER_SIDE_EVENT_COUNT) + .should('exist') + .invoke('text') + .should('not.equal', '0'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts deleted file mode 100644 index ae3ed2010a0ae..0000000000000 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; -import { KQL_SEARCH_BAR } from '../../screens/hosts/main'; - -export const closeFieldsBrowser = () => { - cy.get(KQL_SEARCH_BAR, { timeout: DEFAULT_TIMEOUT }).click(); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts index cbf410b4bc232..eba0c353fbf37 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts @@ -4,24 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; - import { - EVENTS_TAB, + ALL_HOSTS_TAB, AUTHENTICATIONS_TAB, + EVENTS_TAB, UNCOMMON_PROCESSES_TAB, - ALL_HOSTS_TAB, } from '../../screens/hosts/main'; -/** Clicks the Events tab on the hosts page */ -export const openEvents = () => - cy.get(EVENTS_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); +export const openAllHosts = () => cy.get(ALL_HOSTS_TAB).click({ force: true }); -export const openAuthentications = () => - cy.get(AUTHENTICATIONS_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); +export const openAuthentications = () => cy.get(AUTHENTICATIONS_TAB).click({ force: true }); -export const openUncommonProcesses = () => - cy.get(UNCOMMON_PROCESSES_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); +export const openEvents = () => cy.get(EVENTS_TAB).click({ force: true }); -export const openAllHosts = () => - cy.get(ALL_HOSTS_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); +export const openUncommonProcesses = () => cy.get(UNCOMMON_PROCESSES_TAB).click({ force: true }); diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts similarity index 59% rename from x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts index 007a20c770ca0..c44249acdd964 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UNCOMMON_PROCESSES_TABLE } from '../screens/uncommon_processes'; -import { DEFAULT_TIMEOUT } from '../tasks/login'; +import { UNCOMMON_PROCESSES_TABLE } from '../../screens/hosts/uncommon_processes'; export const waitForUncommonProcessesToBeLoaded = () => { - cy.get(UNCOMMON_PROCESSES_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); + cy.get(UNCOMMON_PROCESSES_TABLE).should('exist'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts b/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts index 26b1c0f7e4e39..109cd9c477968 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/inspect.ts @@ -6,24 +6,20 @@ import { INSPECT_BUTTON_ICON, InspectButtonMetadata } from '../screens/inspect'; -import { DEFAULT_TIMEOUT } from '../tasks/login'; - export const closesModal = () => { - cy.get('[data-test-subj="modal-inspect-close"]', { timeout: DEFAULT_TIMEOUT }).click(); + cy.get('[data-test-subj="modal-inspect-close"]').click(); }; export const openStatsAndTables = (table: InspectButtonMetadata) => { if (table.tabId) { cy.get(table.tabId).click({ force: true }); } - cy.get(table.id, { timeout: DEFAULT_TIMEOUT }); + cy.get(table.id); if (table.altInspectId) { - cy.get(table.altInspectId, { timeout: DEFAULT_TIMEOUT }).trigger('click', { + cy.get(table.altInspectId).trigger('click', { force: true, }); } else { - cy.get(`${table.id} ${INSPECT_BUTTON_ICON}`, { - timeout: DEFAULT_TIMEOUT, - }).trigger('click', { force: true }); + cy.get(`${table.id} ${INSPECT_BUTTON_ICON}`).trigger('click', { force: true }); } }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/login.ts b/x-pack/legacy/plugins/siem/cypress/tasks/login.ts index d5bf1178d5bdd..1b982d56d79a4 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/login.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/login.ts @@ -4,10 +4,115 @@ * you may not use this file except in compliance with the Elastic License. */ -import { login } from '../integration/lib/login/helpers'; +import * as yaml from 'js-yaml'; -/** The default time in ms to wait for a Cypress command to complete */ -export const DEFAULT_TIMEOUT = 30 * 1000; +/** + * Credentials in the `kibana.dev.yml` config file will be used to authenticate + * with Kibana when credentials are not provided via environment variables + */ +const KIBANA_DEV_YML_PATH = '../../../../config/kibana.dev.yml'; + +/** + * The configuration path in `kibana.dev.yml` to the username to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username'; + +/** + * The configuration path in `kibana.dev.yml` to the password to be used when + * authenticating with Kibana. + */ +const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password'; + +/** + * The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME'; + +/** + * The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the + * username to be used when authenticating with Kibana + */ +const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; + +/** + * The Kibana server endpoint used for authentication + */ +const LOGIN_API_ENDPOINT = '/internal/security/login'; + +/** + * Authenticates with Kibana using, if specified, credentials specified by + * environment variables. The credentials in `kibana.dev.yml` will be used + * for authentication when the environment variables are unset. + * + * To speed the execution of tests, prefer this non-interactive authentication, + * which is faster than authentication via Kibana's interactive login page. + */ +export const login = () => { + if (credentialsProvidedByEnvironment()) { + loginViaEnvironmentCredentials(); + } else { + loginViaConfig(); + } +}; + +/** + * Returns `true` if the credentials used to login to Kibana are provided + * via environment variables + */ +const credentialsProvidedByEnvironment = (): boolean => + Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null; + +/** + * Authenticates with Kibana by reading credentials from the + * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` + * environment variables, and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaEnvironmentCredentials = () => { + cy.log( + `Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables` + ); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-env' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); +}; + +/** + * Authenticates with Kibana by reading credentials from the + * `kibana.dev.yml` file and POSTing the username and password directly to + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). + */ +const loginViaConfig = () => { + cy.log( + `Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\`` + ); + + // read the login details from `kibana.dev.yaml` + cy.readFile(KIBANA_DEV_YML_PATH).then(kibanaDevYml => { + const config = yaml.safeLoad(kibanaDevYml); + + // programmatically authenticate without interacting with the Kibana login page + cy.request({ + body: { + username: config.elasticsearch.username, + password: config.elasticsearch.password, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`, + }); + }); +}; /** * Authenticates with Kibana, visits the specified `url`, and waits for the @@ -20,5 +125,5 @@ export const loginAndWaitForPage = (url: string) => { cy.viewport('macbook-15'); - cy.contains('a', 'SIEM', { timeout: DEFAULT_TIMEOUT }); + cy.contains('a', 'SIEM'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts b/x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts index c7b031aabc8cd..c7255f3925d9e 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/network/flows.ts @@ -5,8 +5,7 @@ */ import { IPS_TABLE_LOADED } from '../../screens/network/flows'; -import { DEFAULT_TIMEOUT } from '../../tasks/login'; export const waitForIpsTableToBeLoaded = () => { - cy.get(IPS_TABLE_LOADED, { timeout: DEFAULT_TIMEOUT }).should('exist'); + cy.get(IPS_TABLE_LOADED).should('exist'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts b/x-pack/legacy/plugins/siem/cypress/tasks/siem_header.ts similarity index 68% rename from x-pack/legacy/plugins/siem/cypress/tasks/header.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/siem_header.ts index 4067779136d9e..4c43948445c58 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/siem_header.ts @@ -4,26 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMEOUT } from '../tasks/login'; -import { KQL_INPUT, REFRESH_BUTTON } from '../screens/header'; - -export const navigateFromHeaderTo = (page: string) => { - cy.get(page).click({ force: true }); -}; +import { KQL_INPUT, REFRESH_BUTTON } from '../screens/siem_header'; export const clearSearchBar = () => { - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }) + cy.get(KQL_INPUT) .clear() .type('{enter}'); }; +export const kqlSearch = (search: string) => { + cy.get(KQL_INPUT).type(search); +}; + +export const navigateFromHeaderTo = (page: string) => { + cy.get(page).click({ force: true }); +}; + export const refreshPage = () => { cy.get(REFRESH_BUTTON) .click({ force: true }) - .invoke('text', { timeout: DEFAULT_TIMEOUT }) + .invoke('text') .should('not.equal', 'Updating'); }; - -export const kqlSearch = (search: string) => { - cy.get(KQL_INPUT, { timeout: DEFAULT_TIMEOUT }).type(search); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts index 8501bb3d94e26..2bdc62ecbdc03 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts @@ -5,7 +5,10 @@ */ import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/siem_main'; -import { DEFAULT_TIMEOUT } from '../tasks/login'; + +export const openTimeline = () => { + cy.get(TIMELINE_TOGGLE_BUTTON).click(); +}; export const openTimelineIfClosed = () => { cy.get(MAIN_PAGE).then($page => { @@ -14,7 +17,3 @@ export const openTimelineIfClosed = () => { } }); }; - -export const openTimeline = () => { - cy.get(TIMELINE_TOGGLE_BUTTON, { timeout: DEFAULT_TIMEOUT }).click(); -}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts similarity index 79% rename from x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts rename to x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts index 80b5e4379212c..76acdad990a7e 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts @@ -4,78 +4,74 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; - import { + CLOSE_TIMELINE_BTN, + CREATE_NEW_TIMELINE, + ID_FIELD, + ID_HEADER_FIELD, + ID_TOGGLE_FIELD, SEARCH_OR_FILTER_CONTAINER, - TIMELINE_FIELDS_BUTTON, SERVER_SIDE_EVENT_COUNT, - TIMELINE_SETTINGS_ICON, + TIMELINE_FIELDS_BUTTON, TIMELINE_INSPECT_BUTTON, - CREATE_NEW_TIMELINE, - CLOSE_TIMELINE_BTN, - TOGGLE_TIMELINE_EXPAND_EVENT, - TIMESTAMP_TOGGLE_FIELD, - ID_TOGGLE_FIELD, - ID_HEADER_FIELD, - ID_FIELD, + TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, -} from '../../screens/timeline/main'; + TIMESTAMP_TOGGLE_FIELD, + TOGGLE_TIMELINE_EXPAND_EVENT, +} from '../screens/timeline'; -import { drag, drop } from '../../tasks/common'; +import { drag, drop } from '../tasks/common'; export const hostExistsQuery = 'host.name: *'; -export const populateTimeline = () => { - executeTimelineKQL(hostExistsQuery); - cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) - .invoke('text') - .should('be.above', 0); +export const checkIdToggleField = () => { + cy.get(ID_TOGGLE_FIELD).should('not.exist'); + + cy.get(ID_TOGGLE_FIELD).check({ + force: true, + }); }; -export const openTimelineFieldsBrowser = () => { - cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); +export const createNewTimeline = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(CREATE_NEW_TIMELINE).click(); + cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); }; export const executeTimelineKQL = (query: string) => { cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`); }; -export const openTimelineSettings = () => { - cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); +export const expandFirstTimelineEventDetails = () => { + cy.get(TOGGLE_TIMELINE_EXPAND_EVENT) + .first() + .click({ force: true }); +}; + +export const openTimelineFieldsBrowser = () => { + cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); }; export const openTimelineInspectButton = () => { - cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); + cy.get(TIMELINE_INSPECT_BUTTON).should('not.be.disabled'); cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); }; -export const createNewTimeline = () => { - cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); - cy.get(CREATE_NEW_TIMELINE).click(); - cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); +export const openTimelineSettings = () => { + cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); }; -export const expandFirstTimelineEventDetails = () => { - cy.get(TOGGLE_TIMELINE_EXPAND_EVENT, { timeout: DEFAULT_TIMEOUT }) - .first() - .click({ force: true }); +export const populateTimeline = () => { + executeTimelineKQL(hostExistsQuery); + cy.get(SERVER_SIDE_EVENT_COUNT) + .invoke('text') + .should('be.above', 0); }; export const uncheckTimestampToggleField = () => { cy.get(TIMESTAMP_TOGGLE_FIELD).should('exist'); - cy.get(TIMESTAMP_TOGGLE_FIELD, { - timeout: DEFAULT_TIMEOUT, - }).uncheck({ force: true }); -}; - -export const checkIdToggleField = () => { - cy.get(ID_TOGGLE_FIELD).should('not.exist'); - - cy.get(ID_TOGGLE_FIELD).check({ - force: true, - }); + cy.get(TIMESTAMP_TOGGLE_FIELD).uncheck({ force: true }); }; export const dragAndDropIdToggleFieldToTimeline = () => { @@ -89,5 +85,5 @@ export const dragAndDropIdToggleFieldToTimeline = () => { }; export const addNameToTimeline = (name: string) => { - cy.get(TIMELINE_TITLE, { timeout: DEFAULT_TIMEOUT }).type(name); + cy.get(TIMELINE_TITLE).type(name); }; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 164a117b82475..8fdc939e7ee51 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const TIMELINES_PAGE = '/app/siem#/timelines'; -export const OVERVIEW_PAGE = '/app/siem#/overview'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', @@ -14,3 +12,6 @@ export const HOSTS_PAGE_TAB_URLS = { events: '/app/siem#/hosts/events', uncommonProcesses: '/app/siem#/hosts/uncommonProcesses', }; +export const NETWORK_PAGE = '/app/siem#/network'; +export const OVERVIEW_PAGE = '/app/siem#/overview'; +export const TIMELINES_PAGE = '/app/siem#/timelines'; diff --git a/x-pack/legacy/plugins/siem/public/app/app.tsx b/x-pack/legacy/plugins/siem/public/app/app.tsx index 030cb72750649..7413aeab549db 100644 --- a/x-pack/legacy/plugins/siem/public/app/app.tsx +++ b/x-pack/legacy/plugins/siem/public/app/app.tsx @@ -106,6 +106,7 @@ const SiemAppComponent: React.FC = ({ core, plugins }) => storage: new Storage(localStorage), ...core, ...plugins, + savedObjects: core.savedObjects, }} > diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 4f74f9ff2f5d6..b6548e3e950ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -44,7 +44,10 @@ const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailNam children, detailName, }) => ( - + {children ? children : detailName} ); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index 8eb08bd3d62f0..e1b3951a2317d 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -67,7 +67,7 @@ describe('SIEM Navigation', () => { detailName: undefined, navTabs: { case: { - disabled: true, + disabled: false, href: '#/link-to/case', id: 'case', name: 'Case', @@ -160,7 +160,7 @@ describe('SIEM Navigation', () => { filters: [], navTabs: { case: { - disabled: true, + disabled: false, href: '#/link-to/case', id: 'case', name: 'Case', diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 830e00c70975e..bff3bfd62a85c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -5,12 +5,12 @@ */ import { KibanaServices } from '../../lib/kibana'; -import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types'; -import { Direction } from '../../graphql/types'; +import { FetchCasesProps, Case, NewCase, SortFieldCase, AllCases, CaseSnake } from './types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; +import { convertToCamelCase, convertAllCasesToCamel } from './utils'; -export const getCase = async (caseId: string, includeComments: boolean) => { +export const getCase = async (caseId: string, includeComments: boolean): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'GET', asResponse: true, @@ -19,7 +19,7 @@ export const getCase = async (caseId: string, includeComments: boolean) => { }, }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase(response.body!); }; export const getCases = async ({ @@ -31,7 +31,7 @@ export const getCases = async ({ page: 1, perPage: 20, sortField: SortFieldCase.createdAt, - sortOrder: Direction.desc, + sortOrder: 'desc', }, }: FetchCasesProps): Promise => { const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; @@ -46,7 +46,7 @@ export const getCases = async ({ asResponse: true, }); await throwIfNotOk(response.response); - return response.body!; + return convertAllCasesToCamel(response.body!); }; export const createCase = async (newCase: NewCase): Promise => { @@ -56,18 +56,19 @@ export const createCase = async (newCase: NewCase): Promise => { body: JSON.stringify(newCase), }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase(response.body!); }; export const updateCaseProperty = async ( caseId: string, - updatedCase: Partial + updatedCase: Partial, + version: string ): Promise> => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'PATCH', asResponse: true, - body: JSON.stringify(updatedCase), + body: JSON.stringify({ case: updatedCase, version }), }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase, Partial>(response.body!); }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 0f80b2327a30c..1aea0b0f50a89 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Direction } from '../../graphql/types'; interface FormData { isNew?: boolean; } @@ -15,22 +14,35 @@ export interface NewCase extends FormData { title: string; } -export interface Case { +export interface CaseSnake { case_id: string; created_at: string; - created_by: ElasticUser; + created_by: ElasticUserSnake; description: string; state: string; tags: string[]; title: string; updated_at: string; + version?: string; +} + +export interface Case { + caseId: string; + createdAt: string; + createdBy: ElasticUser; + description: string; + state: string; + tags: string[]; + title: string; + updatedAt: string; + version?: string; } export interface QueryParams { page: number; perPage: number; sortField: SortFieldCase; - sortOrder: Direction; + sortOrder: 'asc' | 'desc'; } export interface FilterOptions { @@ -38,21 +50,33 @@ export interface FilterOptions { tags: string[]; } +export interface AllCasesSnake { + cases: CaseSnake[]; + page: number; + per_page: number; + total: number; +} + export interface AllCases { cases: Case[]; page: number; - per_page: number; + perPage: number; total: number; } export enum SortFieldCase { - createdAt = 'created_at', + createdAt = 'createdAt', state = 'state', - updatedAt = 'updated_at', + updatedAt = 'updatedAt', +} + +export interface ElasticUserSnake { + readonly username: string; + readonly full_name?: string | null; } export interface ElasticUser { readonly username: string; - readonly full_name?: string; + readonly fullName?: string | null; } export interface FetchCasesProps { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 8cc961c68fdf0..bf76b69ef22d6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -50,16 +50,16 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { } }; const initialData: Case = { - case_id: '', - created_at: '', - created_by: { + caseId: '', + createdAt: '', + createdBy: { username: '', }, description: '', state: '', tags: [], title: '', - updated_at: '', + updatedAt: '', }; export const useGetCase = (caseId: string): [CaseState] => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index db9c07747ba04..4037823ccfc94 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -17,7 +17,6 @@ import { } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; import { getTypedPayload } from './utils'; -import { Direction } from '../../graphql/types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; @@ -31,16 +30,9 @@ export interface UseGetCasesState { filterOptions: FilterOptions; } -export interface QueryArgs { - page?: number; - perPage?: number; - sortField?: SortFieldCase; - sortOrder?: Direction; -} - export interface Action { type: string; - payload?: AllCases | QueryArgs | FilterOptions; + payload?: AllCases | Partial | FilterOptions; } const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { switch (action.type) { @@ -83,13 +75,13 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS const initialData: AllCases = { page: 0, - per_page: 0, + perPage: 0, total: 0, cases: [], }; export const useGetCases = (): [ UseGetCasesState, - Dispatch>, + Dispatch>>, Dispatch> ] => { const [state, dispatch] = useReducer(dataFetchReducer, { @@ -104,11 +96,11 @@ export const useGetCases = (): [ page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, sortField: SortFieldCase.createdAt, - sortOrder: Direction.desc, + sortOrder: 'desc', }, }); - const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs); - const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); + const [queryParams, setQueryParams] = useState>(state.queryParams); + const [filterQuery, setFilters] = useState(state.filterOptions); const [, dispatchToaster] = useStateToaster(); useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 68592c17e58dc..62e3d87b528c0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -96,7 +96,11 @@ export const useUpdateCase = ( const updateData = async (updateKey: keyof Case) => { dispatch({ type: FETCH_INIT }); try { - const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + const response = await updateCaseProperty( + caseId, + { [updateKey]: state.data[updateKey] }, + state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); dispatch({ type: FETCH_SUCCESS, payload: response }); } catch (error) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts index 8e6eaca1a8f0c..14a3819bdfdad 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -4,4 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ +import { camelCase, isArray, isObject, set } from 'lodash'; +import { AllCases, AllCasesSnake, Case, CaseSnake } from './types'; + export const getTypedPayload = (a: unknown): T => a as T; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = (snakeCase: T): U => + Object.entries(snakeCase).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: AllCasesSnake): AllCases => ({ + cases: snakeCases.cases.map(snakeCase => convertToCamelCase(snakeCase)), + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx new file mode 100644 index 0000000000000..98a67304fcf1f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -0,0 +1,77 @@ +/* + * 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 { SortFieldCase } from '../../../../../containers/case/types'; +import { UseGetCasesState } from '../../../../../containers/case/use_get_cases'; + +export const useGetCasesMockState: UseGetCasesState = { + data: { + cases: [ + { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach', + updatedAt: '2020-02-13T19:44:23.627Z', + }, + { + caseId: '362a5c10-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:13.328Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Bad email', + updatedAt: '2020-02-13T19:44:13.328Z', + }, + { + caseId: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:11.328Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Bad email', + updatedAt: '2020-02-13T19:44:11.328Z', + }, + { + caseId: '31890e90-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:05.563Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'closed', + tags: ['phishing'], + title: 'Uh oh', + updatedAt: '2020-02-18T21:32:24.056Z', + }, + { + caseId: '2f5b3210-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:01.901Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Uh oh', + updatedAt: '2020-02-13T19:44:01.901Z', + }, + ], + page: 1, + perPage: 5, + total: 10, + }, + isLoading: false, + isError: false, + queryParams: { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + filterOptions: { search: '', tags: [] }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 92cd16fd2000e..4c47bf605051d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -14,14 +14,15 @@ import * as i18n from './translations'; export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; -const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = (): CasesColumns[] => [ { name: i18n.CASE_TITLE, render: (theCase: Case) => { - if (theCase.case_id != null && theCase.title != null) { - return {theCase.title}; + if (theCase.caseId != null && theCase.title != null) { + return {theCase.title}; } return getEmptyTagValue(); }, @@ -34,7 +35,11 @@ export const getCasesColumns = (): CasesColumns[] => [ return ( {tags.map((tag: string, i: number) => ( - + {tag} ))} @@ -46,28 +51,39 @@ export const getCasesColumns = (): CasesColumns[] => [ truncateText: true, }, { - field: 'created_at', + field: 'createdAt', name: i18n.CREATED_AT, sortable: true, - render: (createdAt: Case['created_at']) => { + render: (createdAt: Case['createdAt']) => { if (createdAt != null) { - return ; + return ( + + ); } return getEmptyTagValue(); }, }, { - field: 'created_by.username', + field: 'createdBy.username', name: i18n.REPORTER, - render: (createdBy: Case['created_by']['username']) => renderStringField(createdBy), + render: (createdBy: Case['createdBy']['username']) => + renderStringField(createdBy, `case-table-column-username`), }, { - field: 'updated_at', + field: 'updatedAt', name: i18n.LAST_UPDATED, sortable: true, - render: (updatedAt: Case['updated_at']) => { + render: (updatedAt: Case['updatedAt']) => { if (updatedAt != null) { - return ; + return ( + + ); } return getEmptyTagValue(); }, @@ -76,6 +92,6 @@ export const getCasesColumns = (): CasesColumns[] => [ field: 'state', name: i18n.STATE, sortable: true, - render: (state: Case['state']) => renderStringField(state), + render: (state: Case['state']) => renderStringField(state, `case-table-column-state`), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx new file mode 100644 index 0000000000000..5a87cf53142f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { mount } from 'enzyme'; +import moment from 'moment-timezone'; +import { AllCases } from './'; +import { TestProviders } from '../../../../mock'; +import { useGetCasesMockState } from './__mock__'; +import * as apiHook from '../../../../containers/case/use_get_cases'; + +describe('AllCases', () => { + const setQueryParams = jest.fn(); + const setFilters = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + jest + .spyOn(apiHook, 'useGetCases') + .mockReturnValue([useGetCasesMockState, setQueryParams, setFilters]); + moment.tz.setDefault('UTC'); + }); + it('should render AllCases', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .prop('href') + ).toEqual(`#/link-to/case/${useGetCasesMockState.data.cases[0].caseId}`); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].title); + expect( + wrapper + .find(`[data-test-subj="case-table-column-state"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].state); + expect( + wrapper + .find(`span[data-test-subj="case-table-column-tags-0"]`) + .first() + .prop('title') + ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-table-column-username"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdAt"]`) + .first() + .prop('value') + ).toEqual(useGetCasesMockState.data.cases[0].createdAt); + expect( + wrapper + .find(`[data-test-subj="case-table-column-updatedAt"]`) + .first() + .prop('value') + ).toEqual(useGetCasesMockState.data.cases[0].updatedAt); + + expect( + wrapper + .find(`[data-test-subj="case-table-case-count"]`) + .first() + .text() + ).toEqual('Showing 10 cases'); + }); + it('should tableHeaderSortButton AllCases', () => { + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="tableHeaderCell_state_5"] [data-test-subj="tableHeaderSortButton"]') + .simulate('click'); + expect(setQueryParams).toBeCalledWith({ + page: 1, + perPage: 5, + sortField: 'state', + sortOrder: 'asc', + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index b1dd39c95e191..3253a036c2990 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -18,7 +18,6 @@ import * as i18n from './translations'; import { getCasesColumns } from './columns'; import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; -import { Direction } from '../../../../graphql/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; @@ -32,7 +31,16 @@ import { UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; - +const getSortField = (field: string): SortFieldCase => { + if (field === SortFieldCase.createdAt) { + return SortFieldCase.createdAt; + } else if (field === SortFieldCase.state) { + return SortFieldCase.state; + } else if (field === SortFieldCase.updatedAt) { + return SortFieldCase.updatedAt; + } + return SortFieldCase.createdAt; +}; export const AllCases = React.memo(() => { const [ { data, isLoading, queryParams, filterOptions }, @@ -44,24 +52,10 @@ export const AllCases = React.memo(() => { ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; if (sort) { - let newSort; - switch (sort.field) { - case 'state': - newSort = SortFieldCase.state; - break; - case 'created_at': - newSort = SortFieldCase.createdAt; - break; - case 'updated_at': - newSort = SortFieldCase.updatedAt; - break; - default: - newSort = SortFieldCase.createdAt; - } newQueryParams = { ...newQueryParams, - sortField: newSort, - sortOrder: sort.direction as Direction, + sortField: getSortField(sort.field), + sortOrder: sort.direction, }; } if (page) { @@ -114,7 +108,9 @@ export const AllCases = React.memo(() => { - {i18n.SHOWING_CASES(data.total ?? 0)} + + {i18n.SHOWING_CASES(data.total ?? 0)} + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx new file mode 100644 index 0000000000000..7480c4fc4bb2a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseProps } from '../index'; +import { Case } from '../../../../../containers/case/types'; + +export const caseProps: CaseProps = { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + initialData: { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { fullName: null, username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach!!', + updatedAt: '2020-02-19T15:02:57.995Z', + }, + isLoading: false, +}; + +export const data: Case = { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { username: 'elastic', fullName: null }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach!!', + updatedAt: '2020-02-19T15:02:57.995Z', +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx new file mode 100644 index 0000000000000..a9e694bad705d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { mount } from 'enzyme'; +import { CaseComponent } from './'; +import * as apiHook from '../../../../containers/case/use_update_case'; +import { caseProps, data } from './__mock__'; +import { TestProviders } from '../../../../mock'; + +describe('CaseView ', () => { + const dispatchUpdateCaseProperty = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(apiHook, 'useUpdateCase').mockReturnValue([{ data }, dispatchUpdateCaseProperty]); + }); + + it('should render CaseComponent', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="case-view-title"]`) + .first() + .prop('title') + ).toEqual(data.title); + expect( + wrapper + .find(`[data-test-subj="case-view-state"]`) + .first() + .text() + ).toEqual(data.state); + expect( + wrapper + .find(`[data-test-subj="case-view-tag-list"] .euiBadge__text`) + .first() + .text() + ).toEqual(data.tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-view-username"]`) + .first() + .text() + ).toEqual(data.createdBy.username); + expect( + wrapper + .find(`[data-test-subj="case-view-createdAt"]`) + .first() + .prop('value') + ).toEqual(data.createdAt); + expect( + wrapper + .find(`[data-test-subj="case-view-description"]`) + .first() + .prop('raw') + ).toEqual(data.description); + }); + + it('should dispatch update state when button is toggled', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('input[data-test-subj="toggle-case-state"]') + .simulate('change', { target: { value: false } }); + + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + updateKey: 'state', + updateValue: 'closed', + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index a92cf99097fce..5cd71c5855d34 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -60,13 +60,13 @@ const BackgroundWrapper = styled.div` `} `; -interface CasesProps { +export interface CaseProps { caseId: string; initialData: Case; isLoading: boolean; } -export const Cases = React.memo(({ caseId, initialData, isLoading }) => { +export const CaseComponent = React.memo(({ caseId, initialData, isLoading }) => { const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); const [isEditDescription, setIsEditDescription] = useState(false); const [isEditTags, setIsEditTags] = useState(false); @@ -162,14 +162,14 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) ]; const userActions = [ { - avatarName: data.created_by.username, + avatarName: data.createdBy.username, title: (

- {`${data.created_by.username}`} + {`${data.createdBy.username}`} {` ${i18n.ADDED_DESCRIPTION} `}{' '} - + {/* STEPH FIX come back and add label `on` */}

@@ -206,7 +206,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading })
) : ( - + ), }, ]; @@ -229,6 +229,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) href: getCaseUrl(), text: i18n.BACK_TO_ALL, }} + data-test-subj="case-view-title" titleNode={titleNode} title={title} > @@ -239,13 +240,21 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) {i18n.STATUS} - {data.state} + + {data.state} + {i18n.CASE_OPENED} - + @@ -255,6 +264,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) (({ caseId, initialData, isLoading }) - + { ); } - return ; + return ; }); CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 9fd1525003b0b..7d79e287b22e7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -48,8 +48,8 @@ export const Create = React.memo(() => { } }, [form]); - if (newCase && newCase.case_id) { - return ; + if (newCase && newCase.caseId) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index b80ee58f8abbf..33e0a9541c5b4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -42,7 +42,7 @@ const renderUsers = (users: ElasticUser[]) => {

- {username} + {username}

@@ -50,7 +50,7 @@ const renderUsers = (users: ElasticUser[]) => {
window.alert('Email clicked')} + onClick={() => {}} // TO DO iconType="email" aria-label="email" /> diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index 42d333f4f893e..a087dca38de00 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -55,7 +55,7 @@ export const navTabs: SiemNavTab = { id: SiemPageName.case, name: i18n.CASE, href: getCaseUrl(), - disabled: true, + disabled: false, urlKey: 'case', }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index bcf091544e52a..d1f41efdddd14 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -51,6 +51,15 @@ interface GetFilterArgs { index: string[] | undefined | null; } +interface QueryAttributes { + // NOTE: doesn't match Query interface + query: { + query: string; + language: string; + }; + filters: PartialFilter[]; +} + export const getFilter = async ({ filters, index, @@ -72,7 +81,10 @@ export const getFilter = async ({ if (savedId != null && index != null) { try { // try to get the saved object first - const savedObject = await services.savedObjectsClient.get('query', savedId); + const savedObject = await services.savedObjectsClient.get( + 'query', + savedId + ); return getQueryFilter( savedObject.attributes.query.query, savedObject.attributes.query.language, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts index 29d4d2182bd53..c93990e25b52b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts @@ -16,7 +16,9 @@ export const getInputIndex = async ( if (inputIndex != null) { return inputIndex; } else { - const configuration = await services.savedObjectsClient.get('config', version); + const configuration = await services.savedObjectsClient.get<{ + 'siem:defaultIndex': string[]; + }>('config', version); if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 79337aa91b1fe..f7fabfb980195 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,7 +22,17 @@ import { SignalRuleAlertTypeDefinition } from './types'; import { getGapBetweenRuns } from './utils'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; - +interface AlertAttributes { + enabled: boolean; + name: string; + tags: string[]; + createdBy: string; + createdAt: string; + updatedBy: string; + schedule: { + interval: string; + }; +} export const signalRulesAlertType = ({ logger, version, @@ -41,6 +51,7 @@ export const signalRulesAlertType = ({ }), }, ], + defaultActionGroupId: 'default', validate: { params: schema.object({ description: schema.string(), @@ -82,7 +93,7 @@ export const signalRulesAlertType = ({ type, } = params; // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 - const savedObject = await services.savedObjectsClient.get('alert', alertId); + const savedObject = await services.savedObjectsClient.get('alert', alertId); const ruleStatusSavedObjects = await services.savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -123,15 +134,15 @@ export const signalRulesAlertType = ({ ); } - const name: string = savedObject.attributes.name; - const tags: string[] = savedObject.attributes.tags; + const name = savedObject.attributes.name; + const tags = savedObject.attributes.tags; - const createdBy: string = savedObject.attributes.createdBy; - const createdAt: string = savedObject.attributes.createdAt; - const updatedBy: string = savedObject.attributes.updatedBy; - const updatedAt: string = savedObject.updated_at ?? ''; - const interval: string = savedObject.attributes.schedule.interval; - const enabled: boolean = savedObject.attributes.enabled; + const createdBy = savedObject.attributes.createdBy; + const createdAt = savedObject.attributes.createdAt; + const updatedBy = savedObject.attributes.updatedBy; + const updatedAt = savedObject.updated_at ?? ''; + const interval = savedObject.attributes.schedule.interval; + const enabled = savedObject.attributes.enabled; const gap = getGapBetweenRuns({ previousStartedAt: previousStartedAt != null ? moment(previousStartedAt) : null, // TODO: Remove this once previousStartedAt is no longer a string interval, diff --git a/x-pack/legacy/plugins/task_manager/server/migrations.ts b/x-pack/legacy/plugins/task_manager/server/migrations.ts index 7dce763ddf309..97c4f97f59c58 100644 --- a/x-pack/legacy/plugins/task_manager/server/migrations.ts +++ b/x-pack/legacy/plugins/task_manager/server/migrations.ts @@ -7,7 +7,7 @@ import { SavedObject } from '../../../../../src/core/server'; export const migrations = { task: { - '7.4.0': (doc: SavedObject) => ({ + '7.4.0': (doc: SavedObject>) => ({ ...doc, updated_at: new Date().toISOString(), }), @@ -18,7 +18,7 @@ export const migrations = { function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObject) { +}: SavedObject>) { return { ...doc, attributes: { diff --git a/x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts similarity index 51% rename from x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts rename to x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts index 71abaa21bf6bd..b55a4cd5c7bd6 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts +++ b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,5 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]'; -export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; +export function XJsonMode() {} +export function setDependencyCache() {} +export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; +export const SORT_DIRECTION = { ASC: 'asc' }; diff --git a/x-pack/legacy/plugins/transform/public/app/app.tsx b/x-pack/legacy/plugins/transform/public/app/app.tsx index 825c1761bf619..0f21afbcccca8 100644 --- a/x-pack/legacy/plugins/transform/public/app/app.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app.tsx @@ -16,6 +16,7 @@ import { getAppProviders } from './app_dependencies'; import { AuthorizationContext } from './lib/authorization'; import { AppDependencies } from '../shim'; +import { CloneTransformSection } from './sections/clone_transform'; import { CreateTransformSection } from './sections/create_transform'; import { TransformManagementSection } from './sections/transform_management'; @@ -39,6 +40,10 @@ export const App: FC = () => { return (
+ { + test('isMatchAllQuery()', () => { + expect(isMatchAllQuery(defaultQuery)).toBe(false); + expect(isMatchAllQuery(matchAllQuery)).toBe(true); + expect(isMatchAllQuery(simpleQuery)).toBe(false); + }); + test('isSimpleQuery()', () => { expect(isSimpleQuery(defaultQuery)).toBe(true); expect(isSimpleQuery(matchAllQuery)).toBe(false); diff --git a/x-pack/legacy/plugins/transform/public/app/common/request.ts b/x-pack/legacy/plugins/transform/public/app/common/request.ts index 5d508f3d245d3..3b740de177ef8 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/request.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/request.ts @@ -53,6 +53,12 @@ export function isSimpleQuery(arg: any): arg is SimpleQuery { return arg.query_string !== undefined; } +export const matchAllQuery = { match_all: {} }; +export function isMatchAllQuery(query: any): boolean { + return query.match_all !== undefined && Object.keys(query.match_all).length === 0; +} + +export const defaultQuery: PivotQuery = { query_string: { query: '*' } }; export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } diff --git a/x-pack/legacy/plugins/transform/public/app/constants/index.ts b/x-pack/legacy/plugins/transform/public/app/constants/index.ts index 85ffc222f59a2..78b5f018dd782 100644 --- a/x-pack/legacy/plugins/transform/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/constants/index.ts @@ -8,6 +8,7 @@ export const CLIENT_BASE_PATH = '/management/elasticsearch/transform'; export enum SECTION_SLUG { HOME = 'transform_management', + CLONE_TRANSFORM = 'clone_transform', CREATE_TRANSFORM = 'create_transform', } diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_x_json_mode.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_x_json_mode.ts new file mode 100644 index 0000000000000..1017ce198ff29 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_x_json_mode.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState } from 'react'; +import { collapseLiteralStrings, expandLiteralStrings, XJsonMode } from '../../shared_imports'; + +export const xJsonMode = new XJsonMode(); + +export const useXJsonMode = (json: string) => { + const [xJson, setXJson] = useState(expandLiteralStrings(json)); + + return { + xJson, + setXJson, + xJsonMode, + convertToJson: collapseLiteralStrings, + }; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts index 5aec2ac041db3..27556e0d673a8 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts @@ -21,19 +21,33 @@ export interface Privileges { missingPrivileges: MissingPrivileges; } +function isPrivileges(arg: any): arg is Privileges { + return ( + typeof arg === 'object' && + arg !== null && + arg.hasOwnProperty('hasAllPrivileges') && + typeof arg.hasAllPrivileges === 'boolean' && + arg.hasOwnProperty('missingPrivileges') && + typeof arg.missingPrivileges === 'object' && + arg.missingPrivileges !== null + ); +} + export interface MissingPrivileges { [key: string]: string[] | undefined; } export const toArray = (value: string | string[]): string[] => Array.isArray(value) ? value : [value]; -export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privilege) => { +export const hasPrivilegeFactory = (privileges: Privileges | undefined | null) => ( + privilege: Privilege +) => { const [section, requiredPrivilege] = privilege; - if (!privileges.missingPrivileges[section]) { + if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) { // if the section does not exist in our missingPrivileges, everything is OK return true; } - if (privileges.missingPrivileges[section]!.length === 0) { + if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) { return true; } if (requiredPrivilege === '*') { @@ -42,7 +56,9 @@ export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privi } // If we require _some_ privilege, we make sure that the one // we require is *not* in the missingPrivilege array - return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); + return ( + isPrivileges(privileges) && !privileges.missingPrivileges[section]!.includes(requiredPrivilege) + ); }; // create the text for button's tooltips if the user diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 3e55d509a94ab..aa4cd21281e22 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/public'; +import { SavedObjectsClientContract, SimpleSavedObject, IUiSettingsClient } from 'src/core/public'; import { IndexPattern, esQuery, IndexPatternsContract, + IndexPatternAttributes, } from '../../../../../../../../src/plugins/data/public'; +import { matchAllQuery } from '../../common'; + type IndexPatternId = string; type SavedSearchId = string; -let indexPatternCache = []; +let indexPatternCache: Array>> = []; let fullIndexPatterns; let currentIndexPattern = null; let currentSavedSearch = null; @@ -27,7 +30,7 @@ export function loadIndexPatterns( ) { fullIndexPatterns = indexPatterns; return savedObjectsClient - .find({ + .find({ type: 'index-pattern', fields: ['id', 'title', 'type', 'fields'], perPage: 10000, @@ -53,6 +56,10 @@ export function loadIndexPatterns( }); } +export function getIndexPatternIdByTitle(indexPatternTitle: string): string | undefined { + return indexPatternCache.find(d => d?.attributes?.title === indexPatternTitle)?.id; +} + type CombinedQuery = Record<'bool', any> | unknown; export function loadCurrentIndexPattern( @@ -69,12 +76,20 @@ export function loadCurrentSavedSearch(savedSearches: any, savedSearchId: SavedS return currentSavedSearch; } +function isIndexPattern(arg: any): arg is IndexPattern { + return arg !== undefined; +} // Helper for creating the items used for searching and job creation. export function createSearchItems( indexPattern: IndexPattern | undefined, savedSearch: any, config: IUiSettingsClient -) { +): { + indexPattern: IndexPattern; + savedSearch: any; + query: any; + combinedQuery: CombinedQuery; +} { // query is only used by the data visualizer as it needs // a lucene query_string. // Using a blank query will cause match_all:{} to be used @@ -86,17 +101,13 @@ export function createSearchItems( let combinedQuery: CombinedQuery = { bool: { - must: [ - { - match_all: {}, - }, - ], + must: [matchAllQuery], }, }; - if (indexPattern === undefined && savedSearch !== null && savedSearch.id !== undefined) { + if (!isIndexPattern(indexPattern) && savedSearch !== null && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index') as IndexPattern; query = searchSource.getField('query'); const fs = searchSource.getField('filter'); @@ -107,6 +118,10 @@ export function createSearchItems( combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } + if (!isIndexPattern(indexPattern)) { + throw new Error('Index Pattern is not defined.'); + } + return { indexPattern, savedSearch, diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts index 82d5362e21c02..62107cb37ff2c 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { getIndexPatternIdByTitle, loadIndexPatterns } from './common'; export { useKibanaContext, InitializedKibanaContextValue, diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index 5b7702a0193ec..b0a0371d2de86 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -6,30 +6,26 @@ import React, { createContext, useContext, FC } from 'react'; +import { IUiSettingsClient } from 'kibana/public'; + import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/types'; import { IndexPattern, IndexPatternsContract, } from '../../../../../../../../src/plugins/data/public'; -import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server'; - -// set() method is missing in original d.ts -interface KibanaConfigTypeFix extends KibanaConfig { - set(key: string, value: any): void; -} interface UninitializedKibanaContextValue { - initialized: boolean; + initialized: false; } export interface InitializedKibanaContextValue { combinedQuery: any; - currentIndexPattern: IndexPattern; - currentSavedSearch: SavedSearch; indexPatterns: IndexPatternsContract; - initialized: boolean; + initialized: true; kbnBaseUrl: string; - kibanaConfig: KibanaConfigTypeFix; + kibanaConfig: IUiSettingsClient; + currentIndexPattern: IndexPattern; + currentSavedSearch?: SavedSearch; } export type KibanaContextValue = UninitializedKibanaContextValue | InitializedKibanaContextValue; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx index 0a9de49168ad4..d2cf5f2b32910 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx @@ -17,7 +17,7 @@ import { loadCurrentSavedSearch, } from './common'; -import { KibanaContext, KibanaContextValue } from './kibana_context'; +import { InitializedKibanaContextValue, KibanaContext, KibanaContextValue } from './kibana_context'; const indexPatterns = npStart.plugins.data.indexPatterns; const savedObjectsClient = npStart.core.savedObjects.client; @@ -52,20 +52,20 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { const kibanaConfig = npStart.core.uiSettings; - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - fetchedIndexPattern, - fetchedSavedSearch, - kibanaConfig - ); - - const kibanaContext = { + const { + indexPattern: currentIndexPattern, + savedSearch: currentSavedSearch, combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, + } = createSearchItems(fetchedIndexPattern, fetchedSavedSearch, kibanaConfig); + + const kibanaContext: InitializedKibanaContextValue = { indexPatterns, initialized: true, kbnBaseUrl: npStart.core.injectedMetadata.getBasePath(), kibanaConfig, + combinedQuery, + currentIndexPattern, + currentSavedSearch, }; setContextValue(kibanaContext); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx new file mode 100644 index 0000000000000..de96a4de32962 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -0,0 +1,194 @@ +/* + * 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, { useEffect, useState, FC } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiBetaBadge, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { npStart } from 'ui/new_platform'; + +import { useApi } from '../../hooks/use_api'; + +import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { TransformPivotConfig } from '../../common'; +import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; +import { documentationLinksService } from '../../services/documentation'; +import { PrivilegesWrapper } from '../../lib/authorization'; +import { + getIndexPatternIdByTitle, + loadIndexPatterns, + KibanaProvider, + RenderOnlyWithInitializedKibanaContext, +} from '../../lib/kibana'; + +import { Wizard } from '../create_transform/components/wizard'; + +const indexPatterns = npStart.plugins.data.indexPatterns; +const savedObjectsClient = npStart.core.savedObjects.client; + +interface GetTransformsResponseOk { + count: number; + transforms: TransformPivotConfig[]; +} + +interface GetTransformsResponseError { + error: { + msg: string; + path: string; + query: any; + statusCode: number; + response: string; + }; +} + +function isGetTransformsResponseError(arg: any): arg is GetTransformsResponseError { + return arg.error !== undefined; +} + +type GetTransformsResponse = GetTransformsResponseOk | GetTransformsResponseError; + +type Props = RouteComponentProps<{ transformId: string }>; +export const CloneTransformSection: FC = ({ match }) => { + // Set breadcrumb and page title + useEffect(() => { + breadcrumbService.setBreadcrumbs(BREADCRUMB_SECTION.CLONE_TRANSFORM); + docTitleService.setTitle('createTransform'); + }, []); + + const api = useApi(); + + const transformId = match.params.transformId; + + const [transformConfig, setTransformConfig] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [isInitialized, setIsInitialized] = useState(false); + const [savedObjectId, setSavedObjectId] = useState(undefined); + + const fetchTransformConfig = async () => { + try { + const transformConfigs: GetTransformsResponse = await api.getTransforms(transformId); + if (isGetTransformsResponseError(transformConfigs)) { + setTransformConfig(undefined); + setErrorMessage(transformConfigs.error.msg); + setIsInitialized(true); + return; + } + + await loadIndexPatterns(savedObjectsClient, indexPatterns); + const indexPatternTitle = Array.isArray(transformConfigs.transforms[0].source.index) + ? transformConfigs.transforms[0].source.index.join(',') + : transformConfigs.transforms[0].source.index; + const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle); + + if (indexPatternId === undefined) { + throw new Error( + i18n.translate('xpack.transform.clone.errorPromptText', { + defaultMessage: 'Could not fetch the Kibana index pattern ID.', + }) + ); + } + + setSavedObjectId(indexPatternId); + + setTransformConfig(transformConfigs.transforms[0]); + setErrorMessage(undefined); + setIsInitialized(true); + } catch (e) { + setTransformConfig(undefined); + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e, null, 2)); + } + setIsInitialized(true); + } + }; + + useEffect(() => { + fetchTransformConfig(); + // The effect should only be called once. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + + + +

+ +   + +

+
+ + + + + +
+
+ + + {typeof errorMessage !== 'undefined' && ( + +
{JSON.stringify(errorMessage)}
+
+ )} + {savedObjectId !== undefined && isInitialized === true && transformConfig !== undefined && ( + + + + + + )} +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts similarity index 76% rename from x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts rename to x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts index 9c1000db8cb56..fef33d50130a7 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export function callWithInternalUserFactory(elasticsearchPlugin: any): any; +export { CloneTransformSection } from './clone_transform_section'; 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/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx index f326199271592..d7f1d9d099cc3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx @@ -20,6 +20,8 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { const props = { 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/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts index 3fcc3cc15803b..e5c6783db1022 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts @@ -17,6 +17,7 @@ import { getDefaultSelectableFields, getFlattenedFields, isDefaultQuery, + matchAllQuery, EsDoc, EsDocSource, EsFieldName, @@ -75,7 +76,7 @@ export const useSourceIndexData = ( index: indexPattern.title, size: SEARCH_SIZE, // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - body: { query: isDefaultQuery(query) ? { match_all: {} } : query }, + body: { query: isDefaultQuery(query) ? matchAllQuery : query }, }); if (isErrorResponse(resp)) { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index 055f0613e4e44..a0c91c070844b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -19,6 +19,8 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { const props = { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts index 7c5b60715961b..881e8c6b26658 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/index.ts @@ -5,8 +5,9 @@ */ export { + applyTransformConfigToDefineState, + getDefaultStepDefineState, StepDefineExposedState, StepDefineForm, - getDefaultStepDefineState, } from './step_define_form'; export { StepDefineSummary } from './step_define_summary'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx index 4ff1190415dba..a2aa056c1634d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx @@ -27,6 +27,8 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { const groupBy: PivotGroupByConfig = { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 48df371e87664..0311b26304c30 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -25,6 +25,8 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { // Using a wrapping
element because shallow() would fail diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index b8f63ef697e78..1499f99f82824 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEqual } from 'lodash'; import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -27,7 +28,9 @@ import { EuiSwitch, } from '@elastic/eui'; -import { dictionaryToArray } from '../../../../../../common/types/common'; +import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; +import { TransformPivotConfig } from '../../../../common'; +import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { DropDown } from '../aggregation_dropdown'; import { AggListForm } from '../aggregation_list'; import { GroupByListForm } from '../group_by_list'; @@ -43,10 +46,12 @@ import { } from '../../../../lib/kibana'; import { - AggName, - DropDownLabel, getPivotQuery, getPreviewRequestBody, + isMatchAllQuery, + matchAllQuery, + AggName, + DropDownLabel, PivotAggDict, PivotAggsConfig, PivotAggsConfigDict, @@ -55,6 +60,7 @@ import { PivotGroupByConfigDict, PivotSupportedGroupByAggs, PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { getPivotDropdownOptions } from './common'; @@ -89,6 +95,58 @@ export function getDefaultStepDefineState( valid: false, }; } + +export function applyTransformConfigToDefineState( + state: StepDefineExposedState, + transformConfig?: TransformPivotConfig +): StepDefineExposedState { + // apply the transform configuration to wizard DEFINE state + if (transformConfig !== undefined) { + // transform aggregations config to wizard state + state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => { + const aggConfig = transformConfig.pivot.aggregations[aggName] as Dictionary; + const agg = Object.keys(aggConfig)[0]; + aggList[aggName] = { + ...aggConfig[agg], + agg: agg as PIVOT_SUPPORTED_AGGS, + aggName, + dropDownName: aggName, + } as PivotAggsConfig; + return aggList; + }, {} as PivotAggsConfigDict); + + // transform group by config to wizard state + state.groupByList = Object.keys(transformConfig.pivot.group_by).reduce( + (groupByList, groupByName) => { + const groupByConfig = transformConfig.pivot.group_by[groupByName] as Dictionary; + const groupBy = Object.keys(groupByConfig)[0]; + groupByList[groupByName] = { + agg: groupBy as PIVOT_SUPPORTED_GROUP_BY_AGGS, + aggName: groupByName, + dropDownName: groupByName, + ...groupByConfig[groupBy], + } as PivotGroupByConfig; + return groupByList; + }, + {} as PivotGroupByConfigDict + ); + + // only apply the query from the transform config to wizard state if it's not the default query + const query = transformConfig.source.query; + if (query !== undefined && !isEqual(query, matchAllQuery)) { + state.isAdvancedSourceEditorEnabled = true; + state.searchString = ''; + state.searchQuery = query; + state.sourceConfigUpdated = true; + } + + // applying a transform config to wizard state will always result in a valid configuration + state.valid = true; + } + + return state; +} + export function isAggNameConflict( aggName: AggName, aggList: PivotAggsConfigDict, @@ -208,10 +266,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const searchHandler = (d: Record) => { const { filterQuery, queryString } = d; const newSearch = queryString === emptySearch ? defaultSearch : queryString; - const newSearchQuery = - filterQuery.match_all && Object.keys(filterQuery.match_all).length === 0 - ? defaultSearch - : filterQuery; + const newSearchQuery = isMatchAllQuery(filterQuery) ? defaultSearch : filterQuery; setSearchString(newSearch); setSearchQuery(newSearchQuery); }; @@ -329,7 +384,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const [advancedEditorConfigLastApplied, setAdvancedEditorConfigLastApplied] = useState( stringifiedPivotConfig ); - const [advancedEditorConfig, setAdvancedEditorConfig] = useState(stringifiedPivotConfig); + + const { + convertToJson, + setXJson: setAdvancedEditorConfig, + xJson: advancedEditorConfig, + } = useXJsonMode(stringifiedPivotConfig); + // source config const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); const [ @@ -353,7 +414,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange }; const applyAdvancedPivotEditorChanges = () => { - const pivotConfig = JSON.parse(advancedEditorConfig); + const pivotConfig = JSON.parse(convertToJson(advancedEditorConfig)); const newGroupByList: PivotGroupByConfigDict = {}; if (pivotConfig !== undefined && pivotConfig.group_by !== undefined) { @@ -363,10 +424,10 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggConfigKeys = Object.keys(aggConfig); const agg = aggConfigKeys[0] as PivotSupportedGroupByAggs; newGroupByList[aggName] = { + ...aggConfig[agg], agg, aggName, dropDownName: '', - ...aggConfig[agg], }; }); } @@ -380,18 +441,16 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggConfigKeys = Object.keys(aggConfig); const agg = aggConfigKeys[0] as PIVOT_SUPPORTED_AGGS; newAggList[aggName] = { + ...aggConfig[agg], agg, aggName, dropDownName: '', - ...aggConfig[agg], }; }); } setAggList(newAggList); - const prettyPivotConfig = JSON.stringify(pivotConfig, null, 2); - setAdvancedEditorConfig(prettyPivotConfig); - setAdvancedEditorConfigLastApplied(prettyPivotConfig); + setAdvancedEditorConfigLastApplied(advancedEditorConfig); setAdvancedPivotEditorApplyButtonEnabled(false); }; @@ -459,13 +518,11 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange pivotAggsArr ); - const stringifiedPivotConfigUpdate = JSON.stringify(previewRequestUpdate.pivot, null, 2); const stringifiedSourceConfigUpdate = JSON.stringify( previewRequestUpdate.source.query, null, 2 ); - setAdvancedEditorConfig(stringifiedPivotConfigUpdate); setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate); onChange({ @@ -730,7 +787,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange > { @@ -745,7 +802,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange // Try to parse the string passed on from the editor. // If parsing fails, the "Apply"-Button will be disabled try { - JSON.parse(d); + JSON.parse(convertToJson(d)); setAdvancedPivotEditorApplyButtonEnabled(true); } catch (e) { setAdvancedPivotEditorApplyButtonEnabled(false); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 2d9895e8ddcf1..aae366e6008d5 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -26,6 +26,8 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { const groupBy: PivotGroupByConfig = { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts index e454ea32d76ed..5cbdf4500e3c3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts @@ -4,5 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { StepDetailsForm, getDefaultStepDetailsState } from './step_details_form'; +export { + applyTransformConfigToDetailsState, + getDefaultStepDetailsState, + StepDetailsForm, +} from './step_details_form'; export { StepDetailsSummary } from './step_details_summary'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index a01481fde343c..220923f88ed36 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -49,6 +49,22 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState { }; } +export function applyTransformConfigToDetailsState( + state: StepDetailsExposedState, + transformConfig?: TransformPivotConfig +): StepDetailsExposedState { + // apply the transform configuration to wizard DETAILS state + if (transformConfig !== undefined) { + const time = transformConfig.sync?.time; + if (time !== undefined) { + state.continuousModeDateField = time.field; + state.continuousModeDelay = time.delay; + state.isContinuousModeEnabled = true; + } + } + return state; +} + interface Props { overrides?: StepDetailsExposedState; onChange(s: StepDetailsExposedState): void; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 109cf81da6caa..f1861755d9742 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -12,16 +12,22 @@ import { EuiSteps, EuiStepStatus } from '@elastic/eui'; import { useKibanaContext } from '../../../../lib/kibana'; -import { getCreateRequestBody } from '../../../../common'; +import { getCreateRequestBody, TransformPivotConfig } from '../../../../common'; import { + applyTransformConfigToDefineState, + getDefaultStepDefineState, StepDefineExposedState, StepDefineForm, StepDefineSummary, - getDefaultStepDefineState, } from '../step_define'; import { getDefaultStepCreateState, StepCreateForm, StepCreateSummary } from '../step_create'; -import { getDefaultStepDetailsState, StepDetailsForm, StepDetailsSummary } from '../step_details'; +import { + applyTransformConfigToDetailsState, + getDefaultStepDetailsState, + StepDetailsForm, + StepDetailsSummary, +} from '../step_details'; import { WizardNav } from '../wizard_nav'; enum KBN_MANAGEMENT_PAGE_CLASSNAME { @@ -67,17 +73,25 @@ const StepDefine: FC = ({ ); }; -export const Wizard: FC = React.memo(() => { +interface WizardProps { + cloneConfig?: TransformPivotConfig; +} + +export const Wizard: FC = React.memo(({ cloneConfig }) => { const kibanaContext = useKibanaContext(); // The current WIZARD_STEP const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE); // The DEFINE state - const [stepDefineState, setStepDefineState] = useState(getDefaultStepDefineState(kibanaContext)); + const [stepDefineState, setStepDefineState] = useState( + applyTransformConfigToDefineState(getDefaultStepDefineState(kibanaContext), cloneConfig) + ); // The DETAILS state - const [stepDetailsState, setStepDetailsState] = useState(getDefaultStepDetailsState()); + const [stepDetailsState, setStepDetailsState] = useState( + applyTransformConfigToDetailsState(getDefaultStepDetailsState(), cloneConfig) + ); const stepDetails = currentStep === WIZARD_STEPS.DETAILS ? ( 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/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx index 673e60de54572..288630333615a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx @@ -11,6 +11,8 @@ import { CreateTransformButton } from './create_transform_button'; jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List ', () => { test('Minimal initialization', () => { const wrapper = shallow(); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx new file mode 100644 index 0000000000000..40098ac7ef72a --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx @@ -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 React, { FC, useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { + createCapabilityFailureMessage, + AuthorizationContext, +} from '../../../../lib/authorization'; + +import { CLIENT_BASE_PATH, SECTION_SLUG } from '../../../../constants'; + +interface CloneActionProps { + itemId: string; +} + +export const CloneAction: FC = ({ itemId }) => { + const history = useHistory(); + + const { canCreateTransform } = useContext(AuthorizationContext).capabilities; + + const buttonCloneText = i18n.translate('xpack.transform.transformList.cloneActionName', { + defaultMessage: 'Clone', + }); + + function clickHandler() { + history.push(`${CLIENT_BASE_PATH}/${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); + } + + const cloneButton = ( + + {buttonCloneText} + + ); + + if (!canCreateTransform) { + const content = createCapabilityFailureMessage('canStartStopTransform'); + + return ( + + {cloneButton} + + ); + } + + return <>{cloneButton}; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx index 979da13b1f83a..4795a2eb7d7bc 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx @@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const Providers = getAppProviders(createPublicShim()); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx index 71a2eff39506d..5f4d4a71c71eb 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx @@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const Providers = getAppProviders(createPublicShim()); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx index c3b67f7661a1a..f6bb1c8b60667 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx @@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const Providers = getAppProviders(createPublicShim()); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx index 3d847890b2bd5..12e1ba5528c43 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx @@ -8,13 +8,16 @@ import { getActions } from './actions'; jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List Actions', () => { test('getActions()', () => { const actions = getActions({ forceDisable: false }); - expect(actions).toHaveLength(2); + expect(actions).toHaveLength(3); expect(actions[0].isPrimary).toBeTruthy(); expect(typeof actions[0].render).toBe('function'); expect(typeof actions[1].render).toBe('function'); + expect(typeof actions[2].render).toBe('function'); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx index 1773405e36e39..3e3829973e328 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { TransformListRow, TRANSFORM_STATE } from '../../../../common'; +import { CloneAction } from './action_clone'; import { StartAction } from './action_start'; import { StopAction } from './action_stop'; import { DeleteAction } from './action_delete'; @@ -21,6 +22,11 @@ export const getActions = ({ forceDisable }: { forceDisable: boolean }) => { return ; }, }, + { + render: (item: TransformListRow) => { + return ; + }, + }, { render: (item: TransformListRow) => { return ; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx index f16130bfe618b..42f04ed101ad6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx @@ -8,6 +8,8 @@ import { getColumns } from './columns'; jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Job List Columns', () => { test('getColumns()', () => { const columns = getColumns([], () => {}, []); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx index 4f992707cbf1a..7fcaf5e6048f6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx @@ -13,6 +13,8 @@ import { ExpandedRow } from './expanded_row'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List ', () => { // Set timezone to US/Eastern for consistent test results. beforeEach(() => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index 303de6b86ac53..e1a19ddd3c742 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -12,6 +12,8 @@ import { TransformList } from './transform_list'; jest.mock('ui/new_platform'); +jest.mock('../../../../../shared_imports'); + describe('Transform: Transform List ', () => { test('Minimal initialization', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx index c94f5c1d57d49..f68670f0b38b2 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx @@ -14,6 +14,8 @@ jest.mock('ui/timefilter', () => { return {}; }); +jest.mock('../../../shared_imports'); + describe('Transform: ', () => { test('Minimal initialization', () => { const wrapper = shallow(); diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts index 0e0b174f28f99..5a2f698b35154 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts @@ -10,6 +10,7 @@ import { linkToHome } from './links'; export enum BREADCRUMB_SECTION { MANAGEMENT = 'management', HOME = 'home', + CLONE_TRANSFORM = 'cloneTransform', CREATE_TRANSFORM = 'createTransform', } @@ -27,6 +28,7 @@ class BreadcrumbService { private breadcrumbs: Breadcrumbs = { management: [], home: [], + cloneTransform: [], createTransform: [], }; @@ -42,6 +44,12 @@ class BreadcrumbService { href: linkToHome(), }, ]; + this.breadcrumbs.cloneTransform = [ + ...this.breadcrumbs.home, + { + text: textService.breadcrumbs.cloneTransform, + }, + ]; this.breadcrumbs.createTransform = [ ...this.breadcrumbs.home, { diff --git a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts index df1b07e171c62..af4aea7e8db4e 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts @@ -14,6 +14,9 @@ class TextService { home: i18n.translate('xpack.transform.home.breadcrumbTitle', { defaultMessage: 'Transforms', }), + cloneTransform: i18n.translate('xpack.transform.cloneTransform.breadcrumbTitle', { + defaultMessage: 'Clone transform', + }), createTransform: i18n.translate('xpack.transform.createTransform.breadcrumbTitle', { defaultMessage: 'Create transform', }), diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 74e0c9a3878db..248eb00c67dff 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +export { XJsonMode } from '../../../../plugins/es_ui_shared/console_lang/ace/modes/x_json'; +export { + collapseLiteralStrings, + expandLiteralStrings, +} from '../../../../../src/plugins/es_ui_shared/console_lang/lib'; + export { SendRequestConfig, SendRequestResponse, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts index 99c0441063ce6..1d24d190fa9f2 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts @@ -24,7 +24,12 @@ async function getSavedObjectAttributesFromRepo( docID: string ) { try { - return (await savedObjectsRepository.get(docType, docID)).attributes; + return ( + await savedObjectsRepository.get( + docType, + docID + ) + ).attributes; } catch (e) { return null; } diff --git a/x-pack/package.json b/x-pack/package.json index 9d6b5d76a58e7..551e466893f93 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -262,7 +262,7 @@ "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", + "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.keyby": "^4.6.0", "lodash.mean": "^4.1.0", "lodash.topath": "^4.5.2", diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 7ac8ea24b5218..a06048953b62c 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -10,7 +10,7 @@ import { SavedObjectsClientContract, SavedObjectAttributes, SavedObject, -} from '../../../../src/core/server'; +} from 'src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets } from './lib'; @@ -118,7 +118,7 @@ export class ActionsClient { * Update action */ public async update({ id, action }: UpdateOptions): Promise { - const existingObject = await this.savedObjectsClient.get('action', id); + const existingObject = await this.savedObjectsClient.get('action', id); const { actionTypeId } = existingObject.attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); @@ -144,13 +144,13 @@ export class ActionsClient { * Get an action */ public async get({ id }: { id: string }): Promise { - const result = await this.savedObjectsClient.get('action', id); + const result = await this.savedObjectsClient.get('action', id); return { id, - actionTypeId: result.attributes.actionTypeId as string, - name: result.attributes.name as string, - config: result.attributes.config as Record, + actionTypeId: result.attributes.actionTypeId, + name: result.attributes.name, + config: result.attributes.config, }; } diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index c2e8795b5f133..5316e833f33d9 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { GetBasePathFunction } from './types'; +import { GetBasePathFunction, RawAction } from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -59,7 +59,7 @@ export function createExecuteFunction({ }; const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); - const actionSavedObject = await savedObjectsClient.get('action', id); + const actionSavedObject = await savedObjectsClient.get('action', id); const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, params, diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 03b3487f10f1d..8c6969cded85a 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -7,3 +7,8 @@ export * from './alert'; export * from './alert_instance'; export * from './alert_task_instance'; + +export interface ActionGroup { + id: string; + name: string; +} diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index 1a820d55af8a4..f4749772c7004 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -27,7 +27,13 @@ describe('has()', () => { registry.register({ id: 'foo', name: 'Foo', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', executor: jest.fn(), }); expect(registry.has('foo')).toEqual(true); @@ -39,7 +45,13 @@ describe('register()', () => { const alertType = { id: 'test', name: 'Test', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', executor: jest.fn(), }; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -64,14 +76,26 @@ describe('register()', () => { registry.register({ id: 'test', name: 'Test', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', executor: jest.fn(), }); expect(() => registry.register({ id: 'test', name: 'Test', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', executor: jest.fn(), }) ).toThrowErrorMatchingInlineSnapshot(`"Alert type \\"test\\" is already registered."`); @@ -84,13 +108,25 @@ describe('get()', () => { registry.register({ id: 'test', name: 'Test', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', executor: jest.fn(), }); const alertType = registry.get('test'); expect(alertType).toMatchInlineSnapshot(` Object { - "actionGroups": Array [], + "actionGroups": Array [ + Object { + "id": "default", + "name": "Default", + }, + ], + "defaultActionGroupId": "default", "executor": [MockFunction], "id": "test", "name": "Test", @@ -124,6 +160,7 @@ describe('list()', () => { name: 'Test Action Group', }, ], + defaultActionGroupId: 'testActionGroup', executor: jest.fn(), }); const result = registry.list(); @@ -136,6 +173,7 @@ describe('list()', () => { "name": "Test Action Group", }, ], + "defaultActionGroupId": "testActionGroup", "id": "test", "name": "Test", }, diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index b0976d67c70a0..d9045fb986745 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -70,6 +70,7 @@ export class AlertTypeRegistry { id: alertTypeId, name: alertType.name, actionGroups: alertType.actionGroups, + defaultActionGroupId: alertType.defaultActionGroupId, })); } } diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index 629bd05a8c76a..ed6e7562e3acd 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -87,6 +87,7 @@ describe('create()', () => { id: '123', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', async executor() {}, }); }); @@ -522,7 +523,13 @@ describe('create()', () => { alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', validate: { params: schema.object({ param1: schema.string(), @@ -1885,6 +1892,7 @@ describe('update()', () => { id: '123', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', async executor() {}, }); }); @@ -2415,6 +2423,7 @@ describe('update()', () => { id: '123', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', validate: { params: schema.object({ param1: schema.string(), @@ -2647,6 +2656,7 @@ describe('update()', () => { id: '123', name: 'Test', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', async executor() {}, }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index ad308f819da34..9a56781aa1d7d 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -203,7 +203,7 @@ export class AlertsClient { } public async get({ id }: { id: string }): Promise { - const result = await this.savedObjectsClient.get('alert', id); + const result = await this.savedObjectsClient.get('alert', id); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 58b77f0f510f7..727e38d9ba56b 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -12,6 +12,7 @@ export type AlertsClient = PublicMethodsOf; export { AlertType, + ActionGroup, AlertingPlugin, AlertExecutorOptions, AlertActionParams, diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts index e9a61354001f1..67820e44f567e 100644 --- a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts @@ -12,7 +12,13 @@ test('should return passed in params when validation not defined', () => { { id: 'my-alert-type', name: 'My description', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', async executor() {}, }, { @@ -27,7 +33,13 @@ test('should validate and apply defaults when params is valid', () => { { id: 'my-alert-type', name: 'My description', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', validate: { params: schema.object({ param1: schema.string(), @@ -50,7 +62,13 @@ test('should validate and throw error when params is invalid', () => { { id: 'my-alert-type', name: 'My description', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', validate: { params: schema.object({ param1: schema.string(), diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index 3e9f57d55122d..96ee8c5717453 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -40,7 +40,13 @@ describe('listAlertTypesRoute', () => { { id: '1', name: 'name', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', }, ]; @@ -50,7 +56,13 @@ describe('listAlertTypesRoute', () => { Object { "body": Array [ Object { - "actionGroups": Array [], + "actionGroups": Array [ + Object { + "id": "default", + "name": "Default", + }, + ], + "defaultActionGroupId": "default", "id": "1", "name": "name", }, @@ -128,7 +140,13 @@ describe('listAlertTypesRoute', () => { { id: '1', name: 'name', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', }, ]; diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 1b3a9ed8d7b06..32299680a1f8c 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -15,6 +15,7 @@ const alertType: AlertType = { { id: 'default', name: 'Default' }, { id: 'other-group', name: 'Other Group' }, ], + defaultActionGroupId: 'default', executor: jest.fn(), }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index af0e7b658240a..d1bc0de3ae0e2 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -17,6 +17,7 @@ const alertType = { id: 'test', name: 'My test alert', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 34278ee62dbc4..c2a3fbcf38069 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -5,8 +5,7 @@ */ import { pick, mapValues, omit } from 'lodash'; -import { Logger } from '../../../../../src/core/server'; -import { SavedObject } from '../../../../../src/core/server'; +import { Logger, SavedObject } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 4dad46ef32f21..f885b0bdbd046 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -14,6 +14,7 @@ const alertType = { id: 'test', name: 'My test alert', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 202a73f076859..90bc7996729a6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -8,7 +8,7 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../src/core/server'; -import { Alert, AlertActionParams } from '../common'; +import { Alert, AlertActionParams, ActionGroup } from '../common'; import { AlertsClient } from './alerts_client'; export * from '../common'; @@ -52,11 +52,6 @@ export interface AlertExecutorOptions { updatedBy: string | null; } -export interface ActionGroup { - id: string; - name: string; -} - export interface AlertType { id: string; name: string; @@ -64,6 +59,7 @@ export interface AlertType { params?: { validate: (object: any) => any }; }; actionGroups: ActionGroup[]; + defaultActionGroupId: ActionGroup['id']; executor: ({ services, params, state }: AlertExecutorOptions) => Promise; } diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index 0d4809a9ee168..83b8fef48e9be 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -6,8 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { omit } from 'lodash'; -import { KibanaResponseFactory } from 'src/core/server'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { KibanaResponseFactory, SavedObjectsClientContract } from 'src/core/server'; import { RouteInitializerDeps } from '../'; import { CANVAS_TYPE, diff --git a/x-pack/plugins/case/server/config.ts b/x-pack/plugins/case/server/config.ts index a7cb117198f9b..8ff9a793b17dc 100644 --- a/x-pack/plugins/case/server/config.ts +++ b/x-pack/plugins/case/server/config.ts @@ -7,9 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - indexPattern: schema.string({ defaultValue: '.case-test-2' }), - secret: schema.string({ defaultValue: 'Cool secret huh?' }), + enabled: schema.boolean({ defaultValue: true }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 37d087433a2ed..5ca640f0b25c3 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -34,6 +34,7 @@ export class CasePlugin { if (!config.enabled) { return; } + const service = new CaseService(this.log); this.log.debug( diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts index 23283d7f8a5be..25d5cafb4bb06 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts @@ -27,7 +27,8 @@ describe('UPDATE case', () => { id: 'mock-id-1', }, body: { - state: 'closed', + case: { state: 'closed' }, + version: 'WzAsMV0=', }, }); @@ -38,6 +39,42 @@ describe('UPDATE case', () => { expect(typeof response.payload.updated_at).toBe('string'); expect(response.payload.state).toEqual('closed'); }); + it(`Fails with 409 if version does not match`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'patch', + params: { + id: 'mock-id-1', + }, + body: { + case: { state: 'closed' }, + version: 'badv=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + it(`Fails with 406 if updated field is unchanged`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'patch', + params: { + id: 'mock-id-1', + }, + body: { + case: { state: 'open' }, + version: 'WzAsMV0=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(406); + }); it(`Returns an error if updateCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 09075a32ac377..ba26a07dc2394 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { formatAllCases, wrapError } from './utils'; +import { formatAllCases, sortToSnake, wrapError } from './utils'; import { SavedObjectsFindOptionsSchema } from './schema'; import { AllCases } from './types'; @@ -23,7 +23,10 @@ export function initGetAllCasesApi({ caseService, router }: RouteDeps) { const args = request.query ? { client: context.core.savedObjects.client, - options: request.query, + options: { + ...request.query, + sortField: sortToSnake(request.query.sortField ?? ''), + }, } : { client: context.core.savedObjects.client, diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 962dc474254f0..468abc8e7226f 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -41,6 +41,11 @@ export const UpdatedCaseSchema = schema.object({ title: schema.maybe(schema.string()), }); +export const UpdateCaseArguments = schema.object({ + case: UpdatedCaseSchema, + version: schema.string(), +}); + export const SavedObjectsFindOptionsSchema = schema.object({ defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), fields: schema.maybe(schema.arrayOf(schema.string())), diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 2d1a88bcf1429..5f1c207bf9829 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -32,12 +32,14 @@ export interface CaseAttributes extends NewCaseType, SavedObjectAttributes { export type FlattenedCaseSavedObject = CaseAttributes & { case_id: string; + version: string; comments: FlattenedCommentSavedObject[]; }; export type FlattenedCasesSavedObject = Array< CaseAttributes & { case_id: string; + version: string; // TO DO it is partial because we need to add it the commentCount commentCount?: number; } @@ -52,6 +54,7 @@ export interface AllCases { export type FlattenedCommentSavedObject = CommentAttributes & { comment_id: string; + version: string; // TO DO We might want to add the case_id where this comment is related too }; @@ -69,3 +72,13 @@ export interface UpdatedCaseType { title?: UpdatedCaseTyped['title']; updated_at: string; } + +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} + +export type Writable = { + -readonly [K in keyof T]: T[K]; +}; diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts index 2a814c7259e4a..1c1a56dfe9b3a 100644 --- a/x-pack/plugins/case/server/routes/api/update_case.ts +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -5,9 +5,17 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObject } from 'kibana/server'; +import Boom from 'boom'; +import { difference } from 'lodash'; import { wrapError } from './utils'; import { RouteDeps } from '.'; -import { UpdatedCaseSchema } from './schema'; +import { UpdateCaseArguments } from './schema'; +import { CaseAttributes, UpdatedCaseTyped, Writable } from './types'; + +interface UpdateCase extends Writable { + [key: string]: any; +} export function initUpdateCaseApi({ caseService, router }: RouteDeps) { router.patch( @@ -17,23 +25,70 @@ export function initUpdateCaseApi({ caseService, router }: RouteDeps) { params: schema.object({ id: schema.string(), }), - body: UpdatedCaseSchema, + body: UpdateCaseArguments, }, }, async (context, request, response) => { + let theCase: SavedObject; try { - const updatedCase = await caseService.updateCase({ + theCase = await caseService.getCase({ client: context.core.savedObjects.client, caseId: request.params.id, - updatedAttributes: { - ...request.body, - updated_at: new Date().toISOString(), - }, }); - return response.ok({ body: updatedCase.attributes }); } catch (error) { return response.customError(wrapError(error)); } + + if (request.body.version !== theCase.version) { + return response.customError( + wrapError( + Boom.conflict( + 'This case has been updated. Please refresh before saving additional updates.' + ) + ) + ); + } + const currentCase = theCase.attributes; + const updateCase: Partial = Object.entries(request.body.case).reduce( + (acc, [key, value]) => { + const currentValue = currentCase[key]; + if ( + Array.isArray(value) && + Array.isArray(currentValue) && + difference(value, currentValue).length !== 0 + ) { + return { + ...acc, + [key]: value, + }; + } else if (value !== currentCase[key]) { + return { + ...acc, + [key]: value, + }; + } + return acc; + }, + {} + ); + if (Object.keys(updateCase).length > 0) { + try { + const updatedCase = await caseService.updateCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + updatedAttributes: { + ...updateCase, + updated_at: new Date().toISOString(), + }, + }); + return response.ok({ body: { ...updatedCase.attributes, version: updatedCase.version } }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + return response.customError( + wrapError(Boom.notAcceptable('All update fields are identical to current version.')) + ); } ); } diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 51944b04836ab..32de41e1c01c5 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -20,6 +20,7 @@ import { AllCases, NewCaseType, NewCommentType, + SortFieldCase, UserType, } from './types'; @@ -80,6 +81,7 @@ export const flattenCaseSavedObject = ( comments: Array> ): FlattenedCaseSavedObject => ({ case_id: savedObject.id, + version: savedObject.version ? savedObject.version : '0', comments: flattenCommentSavedObjects(comments), ...savedObject.attributes, }); @@ -107,5 +109,21 @@ export const flattenCommentSavedObject = ( savedObject: SavedObject ): FlattenedCommentSavedObject => ({ comment_id: savedObject.id, + version: savedObject.version ? savedObject.version : '0', ...savedObject.attributes, }); + +export const sortToSnake = (sortField: string): SortFieldCase => { + switch (sortField) { + case 'state': + return SortFieldCase.state; + case 'createdAt': + case 'created_at': + return SortFieldCase.createdAt; + case 'updatedAt': + case 'updated_at': + return SortFieldCase.updatedAt; + default: + return SortFieldCase.createdAt; + } +}; diff --git a/x-pack/plugins/case/server/services/tags/read_tags.ts b/x-pack/plugins/case/server/services/tags/read_tags.ts index 58ab99b164cfb..da5905fe4ea35 100644 --- a/x-pack/plugins/case/server/services/tags/read_tags.ts +++ b/x-pack/plugins/case/server/services/tags/read_tags.ts @@ -52,7 +52,7 @@ export const readRawTags = async ({ page: 1, perPage, }); - const tags = await client.find({ + const tags = await client.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], page: 1, diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 1e439b68f8c30..849bfda2bc86f 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -7,7 +7,6 @@ import uuid from 'uuid'; import { SavedObject, - SavedObjectAttributes, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -42,7 +41,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public readonly errors = options.baseClient.errors ) {} - public async create( + public async create( type: string, attributes: T = {} as T, options: SavedObjectsCreateOptions = {} @@ -66,15 +65,15 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon type, await this.options.service.encryptAttributes( { type, id, namespace: options.namespace }, - attributes + attributes as Record ), { ...options, id } ) - ); + ) as SavedObject; } - public async bulkCreate( - objects: SavedObjectsBulkCreateObject[], + public async bulkCreate( + objects: Array>, options?: SavedObjectsBaseOptions ) { // We encrypt attributes for every object in parallel and that can potentially exhaust libuv or @@ -101,14 +100,14 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon id, attributes: await this.options.service.encryptAttributes( { type: object.type, id, namespace: options && options.namespace }, - object.attributes + object.attributes as Record ), - }; + } as SavedObjectsBulkCreateObject; }) ); return this.stripEncryptedAttributesFromBulkResponse( - await this.options.baseClient.bulkCreate(encryptedObjects, options) + await this.options.baseClient.bulkCreate(encryptedObjects, options) ); } @@ -144,28 +143,28 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon return await this.options.baseClient.delete(type, id, options); } - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { return this.stripEncryptedAttributesFromBulkResponse( - await this.options.baseClient.find(options) + await this.options.baseClient.find(options) ); } - public async bulkGet( + public async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options?: SavedObjectsBaseOptions ) { return this.stripEncryptedAttributesFromBulkResponse( - await this.options.baseClient.bulkGet(objects, options) + await this.options.baseClient.bulkGet(objects, options) ); } - public async get(type: string, id: string, options?: SavedObjectsBaseOptions) { + public async get(type: string, id: string, options?: SavedObjectsBaseOptions) { return this.stripEncryptedAttributesFromResponse( - await this.options.baseClient.get(type, id, options) + await this.options.baseClient.get(type, id, options) ); } - public async update( + public async update( type: string, id: string, attributes: Partial, @@ -199,7 +198,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (this.options.service.isRegistered(response.type)) { response.attributes = this.options.service.stripEncryptedAttributes( response.type, - response.attributes + response.attributes as Record ); } @@ -218,7 +217,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon if (this.options.service.isRegistered(savedObject.type)) { savedObject.attributes = this.options.service.stripEncryptedAttributes( savedObject.type, - savedObject.attributes + savedObject.attributes as Record ); } } diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index 80bd2ab7b5171..0dbdc2f3ac7e3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - SavedObject, - SavedObjectAttributes, - SavedObjectsBaseOptions, -} from 'src/core/server'; +import { CoreSetup, SavedObject, SavedObjectsBaseOptions } from 'src/core/server'; import { EncryptedSavedObjectsService } from '../crypto'; import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper'; @@ -20,7 +15,7 @@ interface SetupSavedObjectsParams { } export interface SavedObjectsSetup { - getDecryptedAsInternalUser: ( + getDecryptedAsInternalUser: ( type: string, id: string, options?: SavedObjectsBaseOptions @@ -47,7 +42,7 @@ export function setupSavedObjects({ core.savedObjects.createInternalRepository() ); return { - getDecryptedAsInternalUser: async ( + getDecryptedAsInternalUser: async ( type: string, id: string, options?: SavedObjectsBaseOptions @@ -56,10 +51,10 @@ export function setupSavedObjects({ const savedObject = await internalRepository.get(type, id, options); return { ...savedObject, - attributes: await service.decryptAttributes( + attributes: (await service.decryptAttributes( { type, id, namespace: options && options.namespace }, - savedObject.attributes - ), + savedObject.attributes as Record + )) as T, }; }, }; diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index f0fd9dc610e4e..78be98b7805ba 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -71,7 +71,7 @@ export interface EndpointResultList { } export interface AlertData { - '@timestamp': Date; + '@timestamp': string; agent: { id: string; version: string; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 8530d6206d398..c6c032c273543 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -8,16 +8,14 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { CoreStart, AppMountParameters } from 'kibana/public'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; -import { Route, Switch, BrowserRouter, useLocation } from 'react-router-dom'; -import { Provider, useDispatch } from 'react-redux'; +import { Route, Switch, BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; import { Store } from 'redux'; -import { memo } from 'react'; +import { RouteCapture } from './view/route_capture'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; import { ManagementList } from './view/managing'; import { PolicyList } from './view/policy'; -import { AppAction } from './store/action'; -import { EndpointAppLocation } from './types'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -33,13 +31,6 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou }; } -const RouteCapture = memo(({ children }) => { - const location: EndpointAppLocation = useLocation(); - const dispatch: (action: AppAction) => unknown = useDispatch(); - dispatch({ type: 'userChangedUrl', payload: location }); - return <>{children}; -}); - interface RouterProps { basename: string; store: Store; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts index 6ba7a34ae81d1..0aeeb6881ad96 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts @@ -14,6 +14,7 @@ import { coreMock } from 'src/core/public/mocks'; import { AlertResultList } from '../../../../../common/types'; import { isOnAlertPage } from './selectors'; import { createBrowserHistory } from 'history'; +import { mockAlertResultList } from './mock_alert_result_list'; describe('alert list tests', () => { let store: Store; @@ -28,37 +29,7 @@ describe('alert list tests', () => { describe('when the user navigates to the alert list page', () => { beforeEach(() => { coreStart.http.get.mockImplementation(async () => { - const response: AlertResultList = { - alerts: [ - { - '@timestamp': new Date(1542341895000), - agent: { - id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f', - version: '3.0.0', - }, - event: { - action: 'open', - }, - file_classification: { - malware_classification: { - score: 3, - }, - }, - host: { - hostname: 'HD-c15-bc09190a', - ip: '10.179.244.14', - os: { - name: 'Windows', - }, - }, - thread: {}, - }, - ], - total: 1, - request_page_size: 10, - request_page_index: 0, - result_from_index: 0, - }; + const response: AlertResultList = mockAlertResultList(); return response; }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts index 77708a3c77e2b..5c257c3d65fdc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts @@ -7,37 +7,47 @@ import { Store, createStore, applyMiddleware } from 'redux'; import { History } from 'history'; import { alertListReducer } from './reducer'; -import { AlertListState } from '../../types'; +import { AlertListState, AlertingIndexUIQueryParams } from '../../types'; import { alertMiddlewareFactory } from './middleware'; import { AppAction } from '../action'; import { coreMock } from 'src/core/public/mocks'; import { createBrowserHistory } from 'history'; -import { - urlFromNewPageSizeParam, - paginationDataFromUrl, - urlFromNewPageIndexParam, -} from './selectors'; +import { uiQueryParams } from './selectors'; +import { urlFromQueryParams } from '../../view/alerts/url_from_query_params'; describe('alert list pagination', () => { let store: Store; let coreStart: ReturnType; let history: History; + let queryParams: () => AlertingIndexUIQueryParams; + /** + * Update the history with a new `AlertingIndexUIQueryParams` + */ + let historyPush: (params: AlertingIndexUIQueryParams) => void; beforeEach(() => { coreStart = coreMock.createStart(); history = createBrowserHistory(); + const middleware = alertMiddlewareFactory(coreStart); store = createStore(alertListReducer, applyMiddleware(middleware)); + + history.listen(location => { + store.dispatch({ type: 'userChangedUrl', payload: location }); + }); + + queryParams = () => uiQueryParams(store.getState()); + + historyPush = (nextQueryParams: AlertingIndexUIQueryParams): void => { + return history.push(urlFromQueryParams(nextQueryParams)); + }; }); describe('when the user navigates to the alert list page', () => { describe('when a new page size is passed', () => { beforeEach(() => { - const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState()); - history.push(urlPageSizeSelector(1)); - store.dispatch({ type: 'userChangedUrl', payload: history.location }); + historyPush({ ...queryParams(), page_size: '1' }); }); it('should modify the url correctly', () => { - const actualPaginationQuery = paginationDataFromUrl(store.getState()); - expect(actualPaginationQuery).toMatchInlineSnapshot(` + expect(queryParams()).toMatchInlineSnapshot(` Object { "page_size": "1", } @@ -46,13 +56,10 @@ describe('alert list pagination', () => { describe('and then a new page index is passed', () => { beforeEach(() => { - const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState()); - history.push(urlPageIndexSelector(1)); - store.dispatch({ type: 'userChangedUrl', payload: history.location }); + historyPush({ ...queryParams(), page_index: '1' }); }); it('should modify the url in the correct order', () => { - const actualPaginationQuery = paginationDataFromUrl(store.getState()); - expect(actualPaginationQuery).toMatchInlineSnapshot(` + expect(queryParams()).toMatchInlineSnapshot(` Object { "page_index": "1", "page_size": "1", @@ -64,35 +71,15 @@ describe('alert list pagination', () => { describe('when a new page index is passed', () => { beforeEach(() => { - const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState()); - history.push(urlPageIndexSelector(1)); - store.dispatch({ type: 'userChangedUrl', payload: history.location }); + historyPush({ ...queryParams(), page_index: '1' }); }); it('should modify the url correctly', () => { - const actualPaginationQuery = paginationDataFromUrl(store.getState()); - expect(actualPaginationQuery).toMatchInlineSnapshot(` + expect(queryParams()).toMatchInlineSnapshot(` Object { "page_index": "1", } `); }); - - describe('and then a new page size is passed', () => { - beforeEach(() => { - const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState()); - history.push(urlPageSizeSelector(1)); - store.dispatch({ type: 'userChangedUrl', payload: history.location }); - }); - it('should modify the url correctly and reset index to `0`', () => { - const actualPaginationQuery = paginationDataFromUrl(store.getState()); - expect(actualPaginationQuery).toMatchInlineSnapshot(` - Object { - "page_index": "0", - "page_size": "1", - } - `); - }); - }); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 059507c8f0658..76a6867418bd8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpFetchQuery } from 'kibana/public'; import { AlertResultList } from '../../../../../common/types'; import { AppAction } from '../action'; import { MiddlewareFactory, AlertListState } from '../../types'; -import { isOnAlertPage, paginationDataFromUrl } from './selectors'; +import { isOnAlertPage, apiQueryParams } from './selectors'; export const alertMiddlewareFactory: MiddlewareFactory = coreStart => { return api => next => async (action: AppAction) => { @@ -16,7 +15,7 @@ export const alertMiddlewareFactory: MiddlewareFactory = coreSta const state = api.getState(); if (action.type === 'userChangedUrl' && isOnAlertPage(state)) { const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, { - query: paginationDataFromUrl(state) as HttpFetchQuery, + query: apiQueryParams(state), }); api.dispatch({ type: 'serverReturnedAlertsData', payload: response }); } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts new file mode 100644 index 0000000000000..338a1077b58a2 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.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 { AlertResultList } from '../../../../../common/types'; + +export const mockAlertResultList: (options?: { + total?: number; + request_page_size?: number; + request_page_index?: number; +}) => AlertResultList = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const alerts = []; + for (let index = 0; index < actualCountToReturn; index++) { + alerts.push({ + '@timestamp': new Date(1542341895000).toString(), + agent: { + id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f', + version: '3.0.0', + }, + event: { + action: 'open', + }, + file_classification: { + malware_classification: { + score: 3, + }, + }, + host: { + hostname: 'HD-c15-bc09190a', + ip: '10.179.244.14', + os: { + name: 'Windows', + }, + }, + thread: {}, + }); + } + const mock: AlertResultList = { + alerts, + total, + request_page_size: requestPageSize, + request_page_index: requestPageIndex, + result_from_index: 0, + }; + return mock; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 6ad053888729c..3a0461e06538f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; -import { AlertListState } from '../../types'; +import querystring from 'querystring'; +import { + createSelector, + createStructuredSelector as createStructuredSelectorWithBadType, +} from 'reselect'; +import { Immutable } from '../../../../../common/types'; +import { + AlertListState, + AlertingIndexUIQueryParams, + AlertsAPIQueryParams, + CreateStructuredSelector, +} from '../../types'; +const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** * Returns the Alert Data array from state */ @@ -15,14 +26,12 @@ export const alertListData = (state: AlertListState) => state.alerts; /** * Returns the alert list pagination data from state */ -export const alertListPagination = (state: AlertListState) => { - return { - pageIndex: state.request_page_index, - pageSize: state.request_page_size, - resultFromIndex: state.result_from_index, - total: state.total, - }; -}; +export const alertListPagination = createStructuredSelector({ + pageIndex: (state: AlertListState) => state.request_page_index, + pageSize: (state: AlertListState) => state.request_page_size, + resultFromIndex: (state: AlertListState) => state.result_from_index, + total: (state: AlertListState) => state.total, +}); /** * Returns a boolean based on whether or not the user is on the alerts page @@ -32,48 +41,55 @@ export const isOnAlertPage = (state: AlertListState): boolean => { }; /** - * Returns the query object received from parsing the URL query params - */ -export const paginationDataFromUrl = (state: AlertListState): qs.ParsedUrlQuery => { - if (state.location) { - // Removes the `?` from the beginning of query string if it exists - const query = qs.parse(state.location.search.slice(1)); - return { - ...(query.page_size ? { page_size: query.page_size } : {}), - ...(query.page_index ? { page_index: query.page_index } : {}), - }; - } else { - return {}; - } -}; - -/** - * Returns a function that takes in a new page size and returns a new query param string + * Returns the query object received from parsing the browsers URL query params. + * Used to calculate urls for links and such. */ -export const urlFromNewPageSizeParam: ( +export const uiQueryParams: ( state: AlertListState -) => (newPageSize: number) => string = state => { - return newPageSize => { - const urlPaginationData = paginationDataFromUrl(state); - urlPaginationData.page_size = newPageSize.toString(); +) => Immutable = createSelector( + (state: AlertListState) => state.location, + (location: AlertListState['location']) => { + const data: AlertingIndexUIQueryParams = {}; + if (location) { + // Removes the `?` from the beginning of query string if it exists + const query = querystring.parse(location.search.slice(1)); - // Only set the url back to page zero if the user has changed the page index already - if (urlPaginationData.page_index !== undefined) { - urlPaginationData.page_index = '0'; + /** + * Build an AlertingIndexUIQueryParams object with keys from the query. + * If more than one value exists for a key, use the last. + */ + const keys: Array = [ + 'page_size', + 'page_index', + 'selected_alert', + ]; + for (const key of keys) { + const value = query[key]; + if (typeof value === 'string') { + data[key] = value; + } else if (Array.isArray(value)) { + data[key] = value[value.length - 1]; + } + } } - return '?' + qs.stringify(urlPaginationData); - }; -}; + return data; + } +); /** - * Returns a function that takes in a new page index and returns a new query param string + * query params to use when requesting alert data. */ -export const urlFromNewPageIndexParam: ( +export const apiQueryParams: ( state: AlertListState -) => (newPageIndex: number) => string = state => { - return newPageIndex => { - const urlPaginationData = paginationDataFromUrl(state); - urlPaginationData.page_index = newPageIndex.toString(); - return '?' + qs.stringify(urlPaginationData); - }; -}; +) => Immutable = createSelector( + uiQueryParams, + ({ page_size, page_index }) => ({ + page_size, + page_index, + }) +); + +export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector( + uiQueryParams, + ({ selected_alert: selectedAlert }) => selectedAlert !== undefined +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index 3aeeeaf1c09e2..b95ff7cb2d45c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -48,25 +48,36 @@ export const substateMiddlewareFactory = ( }; }; -export const appStoreFactory = (coreStart: CoreStart): Store => { +export const appStoreFactory: ( + /** + * Allow middleware to communicate with Kibana core. + */ + coreStart: CoreStart, + /** + * Create the store without any middleware. This is useful for testing the store w/o side effects. + */ + disableMiddleware?: boolean +) => Store = (coreStart, disableMiddleware = false) => { const store = createStore( appReducer, - composeWithReduxDevTools( - applyMiddleware( - substateMiddlewareFactory( - globalState => globalState.managementList, - managementMiddlewareFactory(coreStart) - ), - substateMiddlewareFactory( - globalState => globalState.policyList, - policyListMiddlewareFactory(coreStart) - ), - substateMiddlewareFactory( - globalState => globalState.alertList, - alertMiddlewareFactory(coreStart) + disableMiddleware + ? undefined + : composeWithReduxDevTools( + applyMiddleware( + substateMiddlewareFactory( + globalState => globalState.managementList, + managementMiddlewareFactory(coreStart) + ), + substateMiddlewareFactory( + globalState => globalState.policyList, + policyListMiddlewareFactory(coreStart) + ), + substateMiddlewareFactory( + globalState => globalState.alertList, + alertMiddlewareFactory(coreStart) + ) + ) ) - ) - ) ); return store; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts index 250cbc6e312ed..9fb12b77e7252 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -72,7 +72,7 @@ describe('endpoint list saga', () => { expect(fakeHttpServices.post).not.toHaveBeenCalled(); dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' }); await sleep(); - expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints', { + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [{ page_index: 0 }, { page_size: 10 }], }), diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts index ae756caf5aa35..754a855c171ad 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts @@ -18,7 +18,7 @@ export const managementMiddlewareFactory: MiddlewareFactory ) { const managementPageIndex = pageIndex(getState()); const managementPageSize = pageSize(getState()); - const response = await coreStart.http.post('/api/endpoint/endpoints', { + const response = await coreStart.http.post('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [ { page_index: managementPageIndex }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index d07521d09a119..bd4838419891d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -10,6 +10,7 @@ import { EndpointMetadata } from '../../../common/types'; import { AppAction } from './store/action'; import { AlertResultList, Immutable } from '../../../common/types'; +export { AppAction }; export type MiddlewareFactory = ( coreStart: CoreStart ) => ( @@ -63,6 +64,9 @@ export interface GlobalState { readonly policyList: PolicyListState; } +/** + * A better type for createStructuredSelector. This doesn't support the options object. + */ export type CreateStructuredSelector = < SelectorMap extends { [key: string]: (...args: never[]) => unknown } >( @@ -76,7 +80,6 @@ export type CreateStructuredSelector = < export interface EndpointAppLocation { pathname: string; search: string; - state: never; hash: string; key?: string; } @@ -85,3 +88,35 @@ export type AlertListData = AlertResultList; export type AlertListState = Immutable & { readonly location?: Immutable; }; + +/** + * Gotten by parsing the URL from the browser. Used to calculate the new URL when changing views. + */ +export interface AlertingIndexUIQueryParams { + /** + * How many items to show in list. + */ + page_size?: string; + /** + * Which page to show. If `page_index` is 1, show page 2. + */ + page_index?: string; + /** + * If any value is present, show the alert detail view for the selected alert. Should be an ID for an alert event. + */ + selected_alert?: string; +} + +/** + * Query params to pass to the alert API when fetching new data. + */ +export interface AlertsAPIQueryParams { + /** + * Number of results to return. + */ + page_size?: string; + /** + * 0-based index of 'page' to return. + */ + page_index?: string; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx new file mode 100644 index 0000000000000..37847553d512a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -0,0 +1,189 @@ +/* + * 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 * as reactTestingLibrary from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { I18nProvider } from '@kbn/i18n/react'; +import { AlertIndex } from './index'; +import { appStoreFactory } from '../../store'; +import { coreMock } from 'src/core/public/mocks'; +import { fireEvent, waitForElement, act } from '@testing-library/react'; +import { RouteCapture } from '../route_capture'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { AppAction } from '../../types'; +import { mockAlertResultList } from '../../store/alerts/mock_alert_result_list'; + +describe('when on the alerting page', () => { + let render: () => reactTestingLibrary.RenderResult; + let history: MemoryHistory; + let store: ReturnType; + + /** + * @testing-library/react provides `queryByTestId`, but that uses the data attribute + * 'data-testid' whereas our FTR and EUI's tests all use 'data-test-subj'. While @testing-library/react + * could be configured to use 'data-test-subj', it is not currently configured that way. + * + * This provides an equivalent function to `queryByTestId` but that uses our 'data-test-subj' attribute. + */ + let queryByTestSubjId: ( + renderResult: reactTestingLibrary.RenderResult, + testSubjId: string + ) => Promise; + + beforeEach(async () => { + /** + * Create a 'history' instance that is only in-memory and causes no side effects to the testing environment. + */ + history = createMemoryHistory(); + /** + * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. + */ + store = appStoreFactory(coreMock.createStart(), true); + /** + * Render the test component, use this after setting up anything in `beforeEach`. + */ + render = () => { + /** + * Provide the store via `Provider`, and i18n APIs via `I18nProvider`. + * Use react-router via `Router`, passing our in-memory `history` instance. + * Use `RouteCapture` to emit url-change actions when the URL is changed. + * Finally, render the `AlertIndex` component which we are testing. + */ + return reactTestingLibrary.render( + + + + + + + + + + ); + }; + queryByTestSubjId = async (renderResult, testSubjId) => { + return await waitForElement( + /** + * Use document.body instead of container because EUI renders things like popover out of the DOM heirarchy. + */ + () => document.body.querySelector(`[data-test-subj="${testSubjId}"]`), + { + container: renderResult.container, + } + ); + }; + }); + it('should show a data grid', async () => { + await render().findByTestId('alertListGrid'); + }); + describe('when there is no selected alert in the url', () => { + it('should not show the flyout', () => { + expect(render().queryByTestId('alertDetailFlyout')).toBeNull(); + }); + describe('when data loads', () => { + beforeEach(() => { + /** + * Dispatch the `serverReturnedAlertsData` action, which is normally dispatched by the middleware + * after interacting with the server. + */ + reactTestingLibrary.act(() => { + const action: AppAction = { + type: 'serverReturnedAlertsData', + payload: mockAlertResultList(), + }; + store.dispatch(action); + }); + }); + it('should render the alert summary row in the grid', async () => { + const renderResult = render(); + const rows = await renderResult.findAllByRole('row'); + + /** + * There should be a 'row' which is the header, and + * row which is the alert item. + */ + expect(rows).toHaveLength(2); + }); + describe('when the user has clicked the alert type in the grid', () => { + let renderResult: reactTestingLibrary.RenderResult; + beforeEach(async () => { + renderResult = render(); + /** + * This is the cell with the alert type, it has a link. + */ + fireEvent.click(await renderResult.findByTestId('alertTypeCellLink')); + }); + it('should show the flyout', async () => { + await renderResult.findByTestId('alertDetailFlyout'); + }); + }); + }); + }); + describe('when there is a selected alert in the url', () => { + beforeEach(() => { + reactTestingLibrary.act(() => { + history.push({ + ...history.location, + search: '?selected_alert=1', + }); + }); + }); + it('should show the flyout', async () => { + await render().findByTestId('alertDetailFlyout'); + }); + describe('when the user clicks the close button on the flyout', () => { + let renderResult: reactTestingLibrary.RenderResult; + beforeEach(async () => { + renderResult = render(); + /** + * Use our helper function to find the flyout's close button, as it uses a different test ID attribute. + */ + const closeButton = await queryByTestSubjId(renderResult, 'euiFlyoutCloseButton'); + if (closeButton) { + fireEvent.click(closeButton); + } + }); + it('should no longer show the flyout', () => { + expect(render().queryByTestId('alertDetailFlyout')).toBeNull(); + }); + }); + }); + describe('when the url has page_size=1 and a page_index=1', () => { + beforeEach(() => { + reactTestingLibrary.act(() => { + history.push({ + ...history.location, + search: '?page_size=1&page_index=1', + }); + }); + }); + describe('when the user changes page size to 10', () => { + beforeEach(async () => { + const renderResult = render(); + const paginationButton = await queryByTestSubjId( + renderResult, + 'tablePaginationPopoverButton' + ); + if (paginationButton) { + act(() => { + fireEvent.click(paginationButton); + }); + } + const show10RowsButton = await queryByTestSubjId(renderResult, 'tablePagination-10-rows'); + if (show10RowsButton) { + act(() => { + fireEvent.click(show10RowsButton); + }); + } + }); + it('should have a page_index of 0', () => { + expect(history.location.search).toBe('?page_size=10'); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 045b82200b11b..6f88727575557 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -6,16 +6,30 @@ import { memo, useState, useMemo, useCallback } from 'react'; import React from 'react'; -import { EuiDataGrid, EuiDataGridColumn, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { + EuiDataGrid, + EuiDataGridColumn, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiBadge, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; +import { useHistory, Link } from 'react-router-dom'; +import { FormattedDate } from 'react-intl'; +import { urlFromQueryParams } from './url_from_query_params'; +import { AlertData } from '../../../../../common/types'; import * as selectors from '../../store/alerts/selectors'; import { useAlertListSelector } from './hooks/use_alerts_selector'; export const AlertIndex = memo(() => { const history = useHistory(); - const columns: EuiDataGridColumn[] = useMemo(() => { + const columns = useMemo((): EuiDataGridColumn[] => { return [ { id: 'alert_type', @@ -69,22 +83,48 @@ export const AlertIndex = memo(() => { }, []); const { pageIndex, pageSize, total } = useAlertListSelector(selectors.alertListPagination); - const urlFromNewPageSizeParam = useAlertListSelector(selectors.urlFromNewPageSizeParam); - const urlFromNewPageIndexParam = useAlertListSelector(selectors.urlFromNewPageIndexParam); const alertListData = useAlertListSelector(selectors.alertListData); + const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert); + const queryParams = useAlertListSelector(selectors.uiQueryParams); const onChangeItemsPerPage = useCallback( - newPageSize => history.push(urlFromNewPageSizeParam(newPageSize)), - [history, urlFromNewPageSizeParam] + newPageSize => { + const newQueryParms = { ...queryParams }; + newQueryParms.page_size = newPageSize; + delete newQueryParms.page_index; + const relativeURL = urlFromQueryParams(newQueryParms); + return history.push(relativeURL); + }, + [history, queryParams] ); const onChangePage = useCallback( - newPageIndex => history.push(urlFromNewPageIndexParam(newPageIndex)), - [history, urlFromNewPageIndexParam] + newPageIndex => { + return history.push( + urlFromQueryParams({ + ...queryParams, + page_index: newPageIndex, + }) + ); + }, + [history, queryParams] ); const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id)); + const handleFlyoutClose = useCallback(() => { + const { selected_alert, ...paramsWithoutSelectedAlert } = queryParams; + history.push(urlFromQueryParams(paramsWithoutSelectedAlert)); + }, [history, queryParams]); + + const datesForRows: Map = useMemo(() => { + return new Map( + alertListData.map(alertData => { + return [alertData, new Date(alertData['@timestamp'])]; + }) + ); + }, [alertListData]); + const renderCellValue = useMemo(() => { return ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { if (rowIndex > total) { @@ -94,11 +134,18 @@ export const AlertIndex = memo(() => { const row = alertListData[rowIndex % pageSize]; if (columnId === 'alert_type') { - return i18n.translate( - 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', - { - defaultMessage: 'Malicious File', - } + return ( + + {i18n.translate( + 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', + { + defaultMessage: 'Malicious File', + } + )} + ); } else if (columnId === 'event_type') { return row.event.action; @@ -109,7 +156,31 @@ export const AlertIndex = memo(() => { } else if (columnId === 'host_name') { return row.host.hostname; } else if (columnId === 'timestamp') { - return row['@timestamp']; + const date = datesForRows.get(row)!; + if (date && isFinite(date.getTime())) { + return ( + + ); + } else { + return ( + + {i18n.translate( + 'xpack.endpoint.application.endpoint.alerts.alertDate.timestampInvalidLabel', + { + defaultMessage: 'invalid', + } + )} + + ); + } } else if (columnId === 'archived') { return null; } else if (columnId === 'malware_score') { @@ -117,7 +188,7 @@ export const AlertIndex = memo(() => { } return null; }; - }, [alertListData, pageSize, total]); + }, [alertListData, datesForRows, pageSize, queryParams, total]); const pagination = useMemo(() => { return { @@ -130,23 +201,43 @@ export const AlertIndex = memo(() => { }, [onChangeItemsPerPage, onChangePage, pageIndex, pageSize]); return ( - - - - - - - + <> + {hasSelectedAlert && ( + + + +

+ {i18n.translate('xpack.endpoint.application.endpoint.alerts.detailsTitle', { + defaultMessage: 'Alert Details', + })} +

+
+
+ +
+ )} + + + + ({ + visibleColumns, + setVisibleColumns, + }), + [setVisibleColumns, visibleColumns] + )} + renderCellValue={renderCellValue} + pagination={pagination} + data-test-subj="alertListGrid" + data-testid="alertListGrid" + /> + + + + ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/url_from_query_params.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/url_from_query_params.ts new file mode 100644 index 0000000000000..e037d000e6e8f --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/url_from_query_params.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import querystring from 'querystring'; +import { AlertingIndexUIQueryParams, EndpointAppLocation } from '../../types'; + +/** + * Return a relative URL for `AlertingIndexUIQueryParams`. + * usage: + * + * ```ts + * // Replace this with however you get state, e.g. useSelector in react + * const queryParams = selectors.uiQueryParams(store.getState()) + * + * // same as current url, but page_index is now 3 + * const relativeURL = urlFromQueryParams({ ...queryParams, page_index: 3 }) + * + * // now use relativeURL in the 'href' of a link, the 'to' of a react-router-dom 'Link' or history.push, history.replace + * ``` + */ +export function urlFromQueryParams( + queryParams: AlertingIndexUIQueryParams +): Partial { + const search = querystring.stringify(queryParams); + return { + search, + }; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/route_capture.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/route_capture.tsx new file mode 100644 index 0000000000000..28d2019b56888 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/route_capture.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, { memo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { EndpointAppLocation, AppAction } from '../types'; + +/** + * This component should be used above all routes, but below the Provider. + * It dispatches actions when the URL is changed. + */ +export const RouteCapture = memo(({ children }) => { + const location: EndpointAppLocation = useLocation(); + const dispatch: (action: AppAction) => unknown = useDispatch(); + dispatch({ type: 'userChangedUrl', payload: location }); + return <>{children}; +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 4d12e656205fa..25d08a8c347ed 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -31,7 +31,6 @@ export const inverseProjectionMatrix = composeSelectors( /** * The scale by which world values are scaled when rendered. - * TODO make it a number */ export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 85e1d4e694b15..f4abb51f062f2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * This import must be hoisted as it uses `jest.mock`. Is there a better way? Mocking is not good. - */ import React from 'react'; import { render, act, RenderResult, fireEvent } from '@testing-library/react'; import { useCamera } from './use_camera'; diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index afed5199b7d72..aef85f39e0382 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -10,7 +10,7 @@ import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; import { addRoutes } from './routes'; -import { registerEndpointRoutes } from './routes/endpoints'; +import { registerEndpointRoutes } from './routes/metadata'; import { registerAlertRoutes } from './routes/alerts'; import { registerResolverRoutes } from './routes/resolver'; diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/metadata.test.ts similarity index 95% rename from x-pack/plugins/endpoint/server/routes/endpoints.test.ts rename to x-pack/plugins/endpoint/server/routes/metadata.test.ts index 25c4225495a41..ee374bc1b57d6 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata.test.ts @@ -20,9 +20,9 @@ import { } from '../../../../../src/core/server/mocks'; import { EndpointMetadata, EndpointResultList } from '../../common/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './endpoints'; +import { registerEndpointRoutes } from './metadata'; import { EndpointConfigSchema } from '../config'; -import * as data from '../test_data/all_endpoints_data.json'; +import * as data from '../test_data/all_metadata_data.json'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -54,7 +54,7 @@ describe('test endpoint route', () => { >; mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith('/api/endpoint/endpoints') + path.startsWith('/api/endpoint/metadata') )!; await routeHandler( @@ -96,7 +96,7 @@ describe('test endpoint route', () => { Promise.resolve((data as unknown) as SearchResponse) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith('/api/endpoint/endpoints') + path.startsWith('/api/endpoint/metadata') )!; await routeHandler( @@ -143,7 +143,7 @@ describe('test endpoint route', () => { Promise.resolve((data as unknown) as SearchResponse) ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith('/api/endpoint/endpoints') + path.startsWith('/api/endpoint/metadata') )!; await routeHandler( @@ -208,7 +208,7 @@ describe('test endpoint route', () => { }) ); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith('/api/endpoint/endpoints') + path.startsWith('/api/endpoint/metadata') )!; await routeHandler( @@ -239,7 +239,7 @@ describe('test endpoint route', () => { >; mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith('/api/endpoint/endpoints') + path.startsWith('/api/endpoint/metadata') )!; await routeHandler( diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/metadata.ts similarity index 90% rename from x-pack/plugins/endpoint/server/routes/endpoints.ts rename to x-pack/plugins/endpoint/server/routes/metadata.ts index 054172a7f258a..278cfac020a3b 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata.ts @@ -9,9 +9,9 @@ import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; import { - kibanaRequestToEndpointListQuery, - kibanaRequestToEndpointFetchQuery, -} from '../services/endpoint/endpoint_query_builders'; + kibanaRequestToMetadataListESQuery, + kibanaRequestToMetadataGetESQuery, +} from '../services/endpoint/metadata_query_builders'; import { EndpointMetadata, EndpointResultList } from '../../common/types'; import { EndpointAppContext } from '../types'; @@ -22,7 +22,7 @@ interface HitSource { export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.post( { - path: '/api/endpoint/endpoints', + path: '/api/endpoint/metadata', validate: { body: schema.nullable( schema.object({ @@ -53,7 +53,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const queryParams = await kibanaRequestToEndpointListQuery(req, endpointAppContext); + const queryParams = await kibanaRequestToMetadataListESQuery(req, endpointAppContext); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', queryParams @@ -67,7 +67,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp router.get( { - path: '/api/endpoint/endpoints/{id}', + path: '/api/endpoint/metadata/{id}', validate: { params: schema.object({ id: schema.string() }), }, @@ -75,7 +75,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const query = kibanaRequestToEndpointFetchQuery(req, endpointAppContext); + const query = kibanaRequestToMetadataGetESQuery(req, endpointAppContext); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query diff --git a/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.test.ts index a4d7de8fdcfdb..3ef1142b9ce46 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.test.ts @@ -16,26 +16,36 @@ describe('test query builder', () => { config: () => Promise.resolve(EndpointConfigSchema.validate({})), }; const queryParams = await getPagingProperties(mockRequest, mockCtx); - const query = await buildAlertListESQuery(queryParams); + const query = buildAlertListESQuery(queryParams); - expect(query).toEqual({ - body: { - query: { - match_all: {}, - }, - sort: [ - { - '@timestamp': { - order: 'desc', + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "query": Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "event.kind": "alert", + }, + }, + ], }, }, - ], - track_total_hits: 10000, - }, - from: 0, - size: 10, - index: 'my-index', - } as Record); + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + "track_total_hits": 10000, + }, + "from": 0, + "index": "my-index", + "size": 10, + } + `); }); it('should adjust track_total_hits for deep pagination', async () => { const mockRequest = httpServerMock.createKibanaRequest({ @@ -49,26 +59,36 @@ describe('test query builder', () => { config: () => Promise.resolve(EndpointConfigSchema.validate({})), }; const queryParams = await getPagingProperties(mockRequest, mockCtx); - const query = await buildAlertListESQuery(queryParams); + const query = buildAlertListESQuery(queryParams); - expect(query).toEqual({ - body: { - query: { - match_all: {}, - }, - sort: [ - { - '@timestamp': { - order: 'desc', + expect(query).toMatchInlineSnapshot(` + Object { + "body": Object { + "query": Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "event.kind": "alert", + }, + }, + ], }, }, - ], - track_total_hits: 12000, - }, - from: 10000, - size: 1000, - index: 'my-index', - } as Record); + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + "track_total_hits": 12000, + }, + "from": 10000, + "index": "my-index", + "size": 1000, + } + `); }); }); }); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.ts index a20f2ae1cdecd..e56ae43ef5c76 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/alert_query_builders.ts @@ -7,9 +7,9 @@ import { KibanaRequest } from 'kibana/server'; import { EndpointAppConstants } from '../../../common/types'; import { EndpointAppContext, AlertRequestParams, JSONish } from '../../types'; -export const buildAlertListESQuery = async ( +export const buildAlertListESQuery: ( pagingProperties: Record -): Promise => { +) => JSONish = pagingProperties => { const DEFAULT_TOTAL_HITS = 10000; // Calculate minimum total hits set to indicate there's a next page @@ -22,7 +22,15 @@ export const buildAlertListESQuery = async ( body: { track_total_hits: totalHitsMin, query: { - match_all: {}, + bool: { + must: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + }, }, sort: [ { diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts similarity index 79% rename from x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts rename to x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts index bd9986ecf1f97..a3090361d4965 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts @@ -6,17 +6,18 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { - kibanaRequestToEndpointListQuery, - kibanaRequestToEndpointFetchQuery, -} from './endpoint_query_builders'; + kibanaRequestToMetadataListESQuery, + kibanaRequestToMetadataGetESQuery, +} from './metadata_query_builders'; +import { EndpointAppConstants } from '../../../common/types'; describe('query builder', () => { - describe('EndpointListQuery', () => { - it('test default query params for all endpoints when no params or body is provided', async () => { + describe('MetadataListESQuery', () => { + it('test default query params for all endpoints metadata when no params or body is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const query = await kibanaRequestToEndpointListQuery(mockRequest, { + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); @@ -50,19 +51,19 @@ describe('query builder', () => { }, from: 0, size: 10, - index: 'endpoint-agent*', + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, } as Record); }); }); describe('test query builder with kql filter', () => { - it('test default query params for all endpoints when no params or body is provided', async () => { + it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { filter: 'not host.ip:10.140.73.246', }, }); - const query = await kibanaRequestToEndpointListQuery(mockRequest, { + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); @@ -109,12 +110,12 @@ describe('query builder', () => { }, from: 0, size: 10, - index: 'endpoint-agent*', + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, } as Record); }); }); - describe('EndpointFetchQuery', () => { + describe('MetadataGetQuery', () => { it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const mockRequest = httpServerMock.createKibanaRequest({ @@ -122,7 +123,7 @@ describe('query builder', () => { id: mockID, }, }); - const query = kibanaRequestToEndpointFetchQuery(mockRequest, { + const query = kibanaRequestToMetadataGetESQuery(mockRequest, { logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); @@ -132,7 +133,7 @@ describe('query builder', () => { sort: [{ 'event.created': { order: 'desc' } }], size: 1, }, - index: 'endpoint-agent*', + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, }); }); }); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts similarity index 96% rename from x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts rename to x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts index c143b09ec453c..300e837c4af1e 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../common/types'; import { EndpointAppContext } from '../../types'; import { esKuery } from '../../../../../../src/plugins/data/server'; -export const kibanaRequestToEndpointListQuery = async ( +export const kibanaRequestToMetadataListESQuery = async ( request: KibanaRequest, endpointAppContext: EndpointAppContext ): Promise> => { @@ -74,7 +74,7 @@ function buildQueryBody(request: KibanaRequest): Record, endpointAppContext: EndpointAppContext ) => { diff --git a/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json similarity index 100% rename from x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json rename to x-pack/plugins/endpoint/server/test_data/all_metadata_data.json diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts index af50562bd3242..0e40fd335dd31 100644 --- a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts +++ b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore import src from '!!raw-loader!./worker.js'; export const workerModule = { diff --git a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx index 80d1b67e59798..2c553b57dcd48 100644 --- a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx @@ -5,12 +5,13 @@ */ import { useState, useCallback } from 'react'; -import { SavedObjectAttributes, SavedObjectsBatchResponse } from 'src/core/public'; +import { SavedObjectsBatchResponse } from 'src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; export const useBulkGetSavedObject = (type: string) => { const kibana = useKibana(); - const [data, setData] = useState | null>(null); + // TODO: define saved object type + const [data, setData] = useState | null>(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index 1f19671826807..9bd11b6863d93 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -91,6 +91,8 @@ describe('buildFieldList', () => { type: 'indexpattern', attributes: { title: 'testpattern', + type: 'type', + typeMeta: 'typemeta', fields: JSON.stringify([ { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, { name: 'bar' }, diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index a059e32585909..40b2766a647cf 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -9,7 +9,10 @@ import { schema } from '@kbn/config-schema'; import { IScopedClusterClient, SavedObject, RequestHandlerContext } from 'src/core/server'; import { CoreSetup } from 'src/core/server'; import { BASE_API_URL } from '../../common'; -import { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; +import { + IndexPatternsFetcher, + IndexPatternAttributes, +} from '../../../../../src/plugins/data/server'; /** * The number of docs to sample to determine field empty status. @@ -125,7 +128,10 @@ async function fetchFieldExistence({ async function fetchIndexPatternDefinition(indexPatternId: string, context: RequestHandlerContext) { const savedObjectsClient = context.core.savedObjects.client; const requestClient = context.core.elasticsearch.dataClient; - const indexPattern = await savedObjectsClient.get('index-pattern', indexPatternId); + const indexPattern = await savedObjectsClient.get( + 'index-pattern', + indexPatternId + ); const indexPatternTitle = indexPattern.attributes.title; // TODO: maybe don't use IndexPatternsFetcher at all, since we're only using it // to look up field values in the resulting documents. We can accomplish the same @@ -155,7 +161,7 @@ async function fetchIndexPatternDefinition(indexPatternId: string, context: Requ * Exported only for unit tests. */ export function buildFieldList( - indexPattern: SavedObject, + indexPattern: SavedObject, mappings: MappingResult, fieldDescriptors: FieldDescriptor[] ): Field[] { diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 03b1d770fa770..2209e7fb66fcb 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -5,7 +5,6 @@ */ import { - SavedObjectAttributes, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -46,7 +45,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; } - public async create( + public async create( type: string, attributes: T = {} as T, options: SavedObjectsCreateOptions = {} @@ -56,8 +55,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.create(type, attributes, options); } - public async bulkCreate( - objects: SavedObjectsBulkCreateObject[], + public async bulkCreate( + objects: Array>, options: SavedObjectsBaseOptions = {} ) { await this.ensureAuthorized( @@ -76,13 +75,13 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.delete(type, id, options); } - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { await this.ensureAuthorized(options.type, 'find', options.namespace, { options }); - return this.baseClient.find(options); + return this.baseClient.find(options); } - public async bulkGet( + public async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ) { @@ -91,16 +90,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra options, }); - return await this.baseClient.bulkGet(objects, options); + return await this.baseClient.bulkGet(objects, options); } - public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { + public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options }); - return await this.baseClient.get(type, id, options); + return await this.baseClient.get(type, id, options); } - public async update( + public async update( type: string, id: string, attributes: Partial, @@ -116,8 +115,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.update(type, id, attributes, options); } - public async bulkUpdate( - objects: SavedObjectsBulkUpdateObject[] = [], + public async bulkUpdate( + objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { await this.ensureAuthorized( @@ -127,7 +126,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra { objects, options } ); - return await this.baseClient.bulkUpdate(objects, options); + return await this.baseClient.bulkUpdate(objects, options); } private async checkPrivileges(actions: string | string[], namespace?: string) { diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts index 62a78679175b3..534d797123940 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts @@ -5,7 +5,6 @@ */ import { - SavedObjectAttributes, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -65,14 +64,14 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ - public async create( + public async create( type: string, attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { throwErrorIfNamespaceSpecified(options); - return await this.client.create(type, attributes, { + return await this.client.create(type, attributes, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); @@ -87,8 +86,8 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {string} [options.namespace] * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} */ - public async bulkCreate( - objects: SavedObjectsBulkCreateObject[], + public async bulkCreate( + objects: Array>, options: SavedObjectsBaseOptions = {} ) { throwErrorIfNamespaceSpecified(options); @@ -133,10 +132,10 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {object} [options.hasReference] - { type, id } * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - public async find(options: SavedObjectsFindOptions) { + public async find(options: SavedObjectsFindOptions) { throwErrorIfNamespaceSpecified(options); - return await this.client.find({ + return await this.client.find({ ...options, type: (options.type ? coerceToArray(options.type) : this.types).filter( type => type !== 'space' @@ -159,13 +158,13 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * { id: 'foo', type: 'index-pattern' } * ]) */ - public async bulkGet( + public async bulkGet( objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ) { throwErrorIfNamespaceSpecified(options); - return await this.client.bulkGet(objects, { + return await this.client.bulkGet(objects, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); @@ -180,10 +179,10 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {string} [options.namespace] * @returns {promise} - { id, type, version, attributes } */ - public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { + public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { throwErrorIfNamespaceSpecified(options); - return await this.client.get(type, id, { + return await this.client.get(type, id, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); @@ -199,7 +198,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @property {string} [options.namespace] * @returns {promise} */ - public async update( + public async update( type: string, id: string, attributes: Partial, @@ -225,8 +224,8 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * { id: 'foo', type: 'index-pattern', attributes: {} } * ]) */ - public async bulkUpdate( - objects: SavedObjectsBulkUpdateObject[] = [], + public async bulkUpdate( + objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { throwErrorIfNamespaceSpecified(options); diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 6ed6ae16fa0f9..97794311fb3d2 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -18,11 +18,7 @@ import { } from './task'; import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store'; import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks'; -import { - SavedObjectsSerializer, - SavedObjectTypeRegistry, - SavedObjectAttributes, -} from '../../../../src/core/server'; +import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; import { asTaskClaimEvent, TaskEvent } from './task_events'; import { asOk, asErr } from './lib/result_type'; @@ -64,15 +60,13 @@ describe('TaskStore', () => { describe('schedule', () => { async function testSchedule(task: TaskInstance) { const callCluster = jest.fn(); - savedObjectsClient.create.mockImplementation( - async (type: string, attributes: SavedObjectAttributes) => ({ - id: 'testid', - type, - attributes, - references: [], - version: '123', - }) - ); + savedObjectsClient.create.mockImplementation(async (type: string, attributes: any) => ({ + id: 'testid', + type, + attributes, + references: [], + version: '123', + })); const store = new TaskStore({ index: 'tasky', taskManagerId: '', @@ -155,14 +149,14 @@ describe('TaskStore', () => { test('sets runAt to now if not specified', async () => { await testSchedule({ taskType: 'dernstraight', params: {}, state: {} }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes = savedObjectsClient.create.mock.calls[0][1]; + const attributes: any = savedObjectsClient.create.mock.calls[0][1]; expect(new Date(attributes.runAt as string).getTime()).toEqual(mockedDate.getTime()); }); test('ensures params and state are not null', async () => { await testSchedule({ taskType: 'yawn' } as any); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes = savedObjectsClient.create.mock.calls[0][1]; + const attributes: any = savedObjectsClient.create.mock.calls[0][1]; expect(attributes.params).toEqual('{}'); expect(attributes.state).toEqual('{}'); }); @@ -751,7 +745,7 @@ if (doc['task.runAt'].size()!=0) { }; savedObjectsClient.update.mockImplementation( - async (type: string, id: string, attributes: SavedObjectAttributes) => { + async (type: string, id: string, attributes: any) => { return { id, type, diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 3915eeffc5519..0e487386eb04d 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -428,7 +428,8 @@ function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { } export function savedObjectToConcreteTaskInstance( - savedObject: Omit + // TODO: define saved object type + savedObject: Omit, 'references'> ): ConcreteTaskInstance { return { ...savedObject.attributes, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b06645cdfe04..78bb39dd22dea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1618,8 +1618,6 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", "advancedSettings.categorySearchLabel": "カテゴリー", - "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", - "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", "advancedSettings.field.changeImageLinkText": "画像を変更", "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", @@ -1632,17 +1630,10 @@ "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", "advancedSettings.field.offLabel": "オフ", "advancedSettings.field.onLabel": "オン", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", - "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", - "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", - "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", "advancedSettings.pageTitle": "設定", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", @@ -2474,8 +2465,6 @@ "statusPage.statusApp.statusTitle": "プラグインステータス", "statusPage.statusTable.columns.idHeader": "ID", "statusPage.statusTable.columns.statusHeader": "ステータス", - "telemetry.callout.appliesSettingTitle": "この設定は {allOfKibanaText} に適用されます", - "telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて", "telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", "telemetry.callout.clusterStatisticsTitle": "クラスター統計", "telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ecf4dfbb33be6..fc9dacf0e50f7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1618,8 +1618,6 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可视化", "advancedSettings.categorySearchLabel": "类别", - "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", - "advancedSettings.field.cancelEditingButtonLabel": "取消", "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", "advancedSettings.field.changeImageLinkText": "更改图片", "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", @@ -1632,14 +1630,8 @@ "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", "advancedSettings.field.offLabel": "关闭", "advancedSettings.field.onLabel": "开启", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", - "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", - "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", - "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", "advancedSettings.form.clearSearchResultText": "(清除搜索)", "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", @@ -2474,8 +2466,6 @@ "statusPage.statusApp.statusTitle": "插件状态", "statusPage.statusTable.columns.idHeader": "ID", "statusPage.statusTable.columns.statusHeader": "状态", - "telemetry.callout.appliesSettingTitle": "此设置适用于{allOfKibanaText}", - "telemetry.callout.appliesSettingTitle.allOfKibanaText": "所有 Kibana。", "telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。", "telemetry.callout.clusterStatisticsTitle": "集群统计信息", "telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障或者有网络错误。检查 Kibana,然后重新加载页面并重试。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 93a46862f4cd2..1e53e7d983848 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -40,6 +40,7 @@ describe('loadAlertTypes', () => { name: 'Test', actionVariables: ['var1'], actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx index ce524ed8178ee..aa71621f1a914 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx @@ -158,6 +158,7 @@ describe('alert_form', () => { alertTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.list.mockReturnValue([actionType]); actionTypeRegistry.has.mockReturnValue(true); + actionTypeRegistry.get.mockReturnValue(actionType); const initialAlert = ({ name: 'test', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx index 95c049f32436a..18dc88f54e907 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx @@ -40,7 +40,6 @@ import { ActionTypeIndex, ActionConnector, AlertTypeIndex, - ActionGroup, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from '../action_connector_form/connector_add_modal'; @@ -119,7 +118,7 @@ export const AlertForm = ({ const [alertThrottleUnit, setAlertThrottleUnit] = useState('m'); const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState(true); const [connectors, setConnectors] = useState([]); - const [defaultActionGroup, setDefaultActionGroup] = useState(undefined); + const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); const [activeActionItem, setActiveActionItem] = useState( undefined ); @@ -166,13 +165,14 @@ export const AlertForm = ({ ], name: 'threshold', actionVariables: ['ctx.metadata.name', 'ctx.metadata.test'], + defaultActionGroupId: 'alert', }); const index: AlertTypeIndex = {}; for (const alertTypeItem of alertTypes) { index[alertTypeItem.id] = alertTypeItem; } - if (alert.alertTypeId) { - setDefaultActionGroup(index[alert.alertTypeId].actionGroups[0]); + if (alert.alertTypeId && index[alert.alertTypeId]) { + setDefaultActionGroupId(index[alert.alertTypeId].defaultActionGroupId); } setAlertTypesIndex(index); } catch (e) { @@ -251,6 +251,14 @@ export const AlertForm = ({ : null; function addActionType(actionTypeModel: ActionTypeModel) { + if (!defaultActionGroupId) { + toastNotifications!.addDanger({ + title: i18n.translate('xpack.triggersActionsUI.sections.alertForm.unableToAddAction', { + defaultMessage: 'Unable to add action, because default action group is not defined', + }), + }); + return; + } setIsAddActionPanelOpen(false); const actionTypeConnectors = connectors.filter( field => field.actionTypeId === actionTypeModel.id @@ -266,7 +274,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup?.id ?? 'Alert', + group: defaultActionGroupId, params: {}, }); setActionProperty('id', freeConnectors[0].id, alert.actions.length - 1); @@ -278,7 +286,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup?.id ?? 'Alert', + group: defaultActionGroupId, params: {}, }); setActionProperty('id', alert.actions.length, alert.actions.length - 1); @@ -294,12 +302,8 @@ export const AlertForm = ({ onClick={() => { setAlertProperty('alertTypeId', item.id); setAlertTypeModel(item); - if ( - alertTypesIndex && - alertTypesIndex[item.id] && - alertTypesIndex[item.id].actionGroups.length > 0 - ) { - setDefaultActionGroup(alertTypesIndex[item.id].actionGroups[0]); + if (alertTypesIndex && alertTypesIndex[item.id]) { + setDefaultActionGroupId(alertTypesIndex[item.id].defaultActionGroupId); } }} > @@ -356,7 +360,7 @@ export const AlertForm = ({ id, })); const actionTypeRegisterd = actionTypeRegistry.get(actionConnector.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; + if (!actionTypeRegisterd || actionItem.group !== defaultActionGroupId) return null; const ParamsFieldsComponent = actionTypeRegisterd.actionParamsFields; const actionParamsErrors: { errors: IErrorObject } = Object.keys(actionsErrors).length > 0 ? actionsErrors[actionItem.id] : { errors: {} }; @@ -368,7 +372,7 @@ export const AlertForm = ({ id={index.toString()} className="euiAccordionForm" buttonContentClassName="euiAccordionForm__button" - data-test-subj="alertActionAccordion" + data-test-subj={`alertActionAccordion-${defaultActionGroupId}`} buttonContent={ @@ -465,7 +469,9 @@ export const AlertForm = ({ errors={actionParamsErrors.errors} editAction={setActionParamsProperty} messageVariables={ - alertTypesIndex ? alertTypesIndex[alert.alertTypeId].actionVariables : undefined + alertTypesIndex && alertTypesIndex[alert.alertTypeId] + ? alertTypesIndex[alert.alertTypeId].actionVariables + : undefined } defaultMessage={alertTypeModel?.defaultActionMessage ?? undefined} /> @@ -479,7 +485,7 @@ export const AlertForm = ({ ? actionTypesIndex[actionItem.actionTypeId].name : actionItem.actionTypeId; const actionTypeRegisterd = actionTypeRegistry.get(actionItem.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; + if (!actionTypeRegisterd || actionItem.group !== defaultActionGroupId) return null; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index c67954bdc44fd..d2cf2decc4a16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -45,6 +45,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -65,6 +66,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -92,6 +94,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -141,6 +144,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; const actionTypes: ActionType[] = [ @@ -191,6 +195,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -215,6 +220,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -239,6 +245,7 @@ describe('alert_details', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -268,6 +275,7 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -293,6 +301,7 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -318,6 +327,7 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -352,6 +362,7 @@ describe('enable button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -389,6 +400,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -415,6 +427,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -441,6 +454,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -476,6 +490,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; @@ -511,6 +526,7 @@ describe('mute button', () => { id: '.noop', name: 'No Op', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', actionVariables: [], }; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index aa1d3d068ed77..2119c08bedc31 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ActionGroup } from '../../alerting/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { @@ -70,17 +71,16 @@ export interface ActionConnectorTableItem extends ActionConnector { actionType: ActionType['name']; } -export interface ActionGroup { - id: string; - name: string; -} export interface AlertType { id: string; name: string; actionGroups: ActionGroup[]; actionVariables: string[]; + defaultActionGroupId: ActionGroup['id']; } +export type SanitizedAlertType = Omit; + export type AlertWithoutId = Omit; export interface AlertTableItem extends Alert { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index b06e0c9e0f8cd..2e7674f2b3eb7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -206,6 +206,7 @@ export default function(kibana: any) { { id: 'default', name: 'Default' }, { id: 'other', name: 'Other' }, ], + defaultActionGroupId: 'default', async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, @@ -260,6 +261,7 @@ export default function(kibana: any) { { id: 'default', name: 'Default' }, { id: 'other', name: 'Other' }, ], + defaultActionGroupId: 'default', async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, state } = alertExecutorOptions; const group = 'default'; @@ -281,7 +283,13 @@ export default function(kibana: any) { const neverFiringAlertType: AlertType = { id: 'test.never-firing', name: 'Test: Never firing', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) { await services.callCluster('index', { index: params.index, @@ -301,7 +309,13 @@ export default function(kibana: any) { const failingAlertType: AlertType = { id: 'test.failing', name: 'Test: Failing', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) { await services.callCluster('index', { index: params.index, @@ -319,7 +333,13 @@ export default function(kibana: any) { const authorizationAlertType: AlertType = { id: 'test.authorization', name: 'Test: Authorization', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', validate: { params: schema.object({ callClusterAuthorizationIndex: schema.string(), @@ -378,7 +398,13 @@ export default function(kibana: any) { const validationAlertType: AlertType = { id: 'test.validation', name: 'Test: Validation', - actionGroups: [], + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', validate: { params: schema.object({ param1: schema.string(), @@ -390,6 +416,7 @@ export default function(kibana: any) { id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) {}, }; server.newPlatform.setup.plugins.alerting.registerType(alwaysFiringAlertType); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 517a60f77849e..30c1548b7db2a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -41,6 +41,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { ); expect(fixtureAlertType).to.eql({ actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 55570744f6af9..590de1ea7ce0b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -21,6 +21,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts index 238c63640386a..4ffd0c3b6044b 100644 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -10,7 +10,7 @@ export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProvide describe('Endpoint plugin', function() { this.tags(['endpoint']); loadTestFile(require.resolve('./resolver')); - loadTestFile(require.resolve('./endpoints')); + loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./alerts')); }); } diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts similarity index 61% rename from x-pack/test/api_integration/apis/endpoint/endpoints.ts rename to x-pack/test/api_integration/apis/endpoint/metadata.ts index febe5f523cb6f..4b0cc8d93a395 100644 --- a/x-pack/test/api_integration/apis/endpoint/endpoints.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -9,12 +9,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test endpoints api', () => { - describe('POST /api/endpoint/endpoints when index is empty', () => { - it('endpoints api should return empty result when index is empty', async () => { - await esArchiver.unload('endpoint/endpoints/api_feature'); + describe('test metadata api', () => { + describe('POST /api/endpoint/metadata when index is empty', () => { + it('metadata api should return empty result when index is empty', async () => { + await esArchiver.unload('endpoint/metadata/api_feature'); const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send() .expect(200); @@ -25,12 +25,12 @@ export default function({ getService }: FtrProviderContext) { }); }); - describe('POST /api/endpoint/endpoints when index is not empty', () => { - before(() => esArchiver.load('endpoint/endpoints/api_feature')); - after(() => esArchiver.unload('endpoint/endpoints/api_feature')); - it('endpoints api should return one entry for each endpoint with default paging', async () => { + describe('POST /api/endpoint/metadata when index is not empty', () => { + before(() => esArchiver.load('endpoint/metadata/api_feature')); + after(() => esArchiver.unload('endpoint/metadata/api_feature')); + it('metadata api should return one entry for each endpoint with default paging', async () => { const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send() .expect(200); @@ -40,9 +40,9 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_index).to.eql(0); }); - it('endpoints api should return page based on paging properties passed.', async () => { + it('metadata api should return page based on paging properties passed.', async () => { const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ paging_properties: [ @@ -61,12 +61,12 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_index).to.eql(1); }); - /* test that when paging properties produces no result, the total should reflect the actual number of endpoints + /* test that when paging properties produces no result, the total should reflect the actual number of metadata in the index. */ - it('endpoints api should return accurate total endpoints if page index produces no result', async () => { + it('metadata api should return accurate total metadata if page index produces no result', async () => { const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ paging_properties: [ @@ -85,9 +85,9 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_index).to.eql(30); }); - it('endpoints api should return 400 when pagingProperties is below boundaries.', async () => { + it('metadata api should return 400 when pagingProperties is below boundaries.', async () => { const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ paging_properties: [ @@ -103,9 +103,9 @@ export default function({ getService }: FtrProviderContext) { expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [1]'); }); - it('endpoints api should return page based on filters passed.', async () => { + it('metadata api should return page based on filters passed.', async () => { const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ filter: 'not host.ip:10.101.149.26' }) .expect(200); @@ -115,10 +115,10 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_index).to.eql(0); }); - it('endpoints api should return page based on filters and paging passed.', async () => { + it('metadata api should return page based on filters and paging passed.', async () => { const notIncludedIp = '10.101.149.26'; const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ paging_properties: [ @@ -143,10 +143,10 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_index).to.eql(0); }); - it('endpoints api should return page based on host.os.variant filter.', async () => { + it('metadata api should return page based on host.os.variant filter.', async () => { const variantValue = 'Windows Pro'; const { body } = await supertest - .post('/api/endpoint/endpoints') + .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ filter: `host.os.variant.keyword:${variantValue}`, @@ -161,6 +161,40 @@ export default function({ getService }: FtrProviderContext) { expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); }); + + it('metadata api should return the latest event for all the events for an endpoint', async () => { + const targetEndpointIp = '10.192.213.130'; + const { body } = await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send({ + filter: `host.ip:${targetEndpointIp}`, + }) + .expect(200); + expect(body.total).to.eql(1); + const resultIp: string = body.endpoints[0].host.ip.filter( + (ip: string) => ip === targetEndpointIp + ); + expect(resultIp).to.eql([targetEndpointIp]); + expect(body.endpoints[0].event.created).to.eql('2020-01-24T16:06:09.541Z'); + expect(body.endpoints.length).to.eql(1); + expect(body.request_page_size).to.eql(10); + expect(body.request_page_index).to.eql(0); + }); + + it('metadata api should return all endpoints when filter is empty string', async () => { + const { body } = await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send({ + filter: '', + }) + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(3); + expect(body.request_page_size).to.eql(10); + expect(body.request_page_index).to.eql(0); + }); }); }); } diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/management.ts index bac87f34ceb82..4925fa7678ab0 100644 --- a/x-pack/test/functional/apps/endpoint/management.ts +++ b/x-pack/test/functional/apps/endpoint/management.ts @@ -15,7 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Endpoint Management List', function() { this.tags('ciGroup7'); before(async () => { - await esArchiver.load('endpoint/endpoints/api_feature'); + await esArchiver.load('endpoint/metadata/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); }); @@ -25,23 +25,65 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('displays table data', async () => { - const data = await pageObjects.endpoint.getManagementTableData(); - [ - 'Hostnamecadmann-4.example.com', - 'PolicyPolicy Name', - 'Policy StatusPolicy Status', - 'Alerts0', - 'Operating Systemwindows 10.0', - 'IP Address10.192.213.130, 10.70.28.129', - 'Sensor Versionversion', - 'Last Activexxxx', - ].forEach((cellValue, index) => { - expect(data[1][index]).to.equal(cellValue); - }); + const expectedData = [ + [ + 'Hostname', + 'Policy', + 'Policy Status', + 'Alerts', + 'Operating System', + 'IP Address', + 'Sensor Version', + 'Last Active', + ], + [ + 'cadmann-4.example.com', + 'Policy Name', + 'Policy Status', + '0', + 'windows 10.0', + '10.192.213.130, 10.70.28.129', + 'version', + 'xxxx', + ], + [ + 'thurlow-9.example.com', + 'Policy Name', + 'Policy Status', + '0', + 'windows 10.0', + '10.46.229.234', + 'version', + 'xxxx', + ], + [ + 'rezzani-7.example.com', + 'Policy Name', + 'Policy Status', + '0', + 'windows 10.0', + '10.101.149.26, 2606:a000:ffc0:39:11ef:37b9:3371:578c', + 'version', + 'xxxx', + ], + ]; + const tableData = await pageObjects.endpoint.getEndpointAppTableData('managementListTable'); + expect(tableData).to.eql(expectedData); + }); + + it('displays no items found', async () => { + // clear out the data and reload the page + await esArchiver.unload('endpoint/metadata/api_feature'); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); + // get the table data and verify no entries appear + const tableData = await pageObjects.endpoint.getEndpointAppTableData('managementListTable'); + expect(tableData[1][0]).to.equal('No items found'); + // reload the data so the other tests continue to pass + await esArchiver.load('endpoint/metadata/api_feature'); }); after(async () => { - await esArchiver.unload('endpoint/endpoints/api_feature'); + await esArchiver.unload('endpoint/metadata/api_feature'); }); }); }; diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json similarity index 98% rename from x-pack/test/functional/es_archives/endpoint/endpoints/api_feature/data.json rename to x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index 87720b068f0e8..6a7911b5be61f 100644 --- a/x-pack/test/functional/es_archives/endpoint/endpoints/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -110,7 +110,7 @@ "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", "ip": [ "10.101.149.26", - "10.12.85.216" + "2606:a000:ffc0:39:11ef:37b9:3371:578c" ], "mac": [ "e2-6d-f9-0-46-2e" @@ -238,7 +238,7 @@ "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", "ip": [ "10.101.149.26", - "10.12.85.216" + "2606:a000:ffc0:39:11ef:37b9:3371:578c" ], "mac": [ "e2-6d-f9-0-46-2e" @@ -365,7 +365,7 @@ "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", "ip": [ "10.101.149.26", - "10.12.85.216" + "2606:a000:ffc0:39:11ef:37b9:3371:578c" ], "mac": [ "e2-6d-f9-0-46-2e" diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json similarity index 100% rename from x-pack/test/functional/es_archives/endpoint/endpoints/api_feature/mappings.json rename to x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index 54f537dd0e8c3..185b95b00527d 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const table = getService('table'); return { /** @@ -34,8 +34,29 @@ export function EndpointPageProvider({ getService }: FtrProviderContext) { return await testSubjects.getVisibleText('welcomeTitle'); }, - async getManagementTableData() { - return await table.getDataFromTestSubj('managementListTable'); + /** + * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. + * It uses euiTableCellContent to avoid poluting the array data with the euiTableRowCell__mobileHeader data. + * @param dataTestSubj + * @returns Promise + */ + async getEndpointAppTableData(dataTestSubj: string) { + await testSubjects.exists(dataTestSubj); + const hostTable: WebElementWrapper = await testSubjects.find(dataTestSubj); + const $ = await hostTable.parseDomContent(); + return $('tr') + .toArray() + .map(row => + $(row) + .find('.euiTableCellContent') + .toArray() + .map(cell => + $(cell) + .text() + .replace(/ /g, '') + .trim() + ) + ); }, }; } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index 678707af40bae..9069044b83ed9 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -23,6 +23,7 @@ function createNoopAlertType(setupContract: any) { id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', async executor() {}, }; setupContract.registerType(noopAlertType); diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 071067ffa85cb..3529d8f3ae9c9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -59,7 +59,9 @@ export function resolveCopyToSpaceConflictsSuite( .then((response: any) => response.body); }; - const getObjectsAtSpace = async (spaceId: string): Promise<[SavedObject, SavedObject]> => { + const getObjectsAtSpace = async ( + spaceId: string + ): Promise<[SavedObject, SavedObject]> => { const dashboard = await getDashboardAtSpace(spaceId); const visualization = await getVisualizationAtSpace(spaceId); return [dashboard, visualization]; diff --git a/yarn.lock b/yarn.lock index 6b3370407e3b2..8ea23c17b8b8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1680,7 +1680,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f" integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw== @@ -1880,10 +1880,10 @@ dependencies: "@elastic/apm-rum-core" "^4.7.0" -"@elastic/charts@^17.0.2": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-17.0.2.tgz#0bd2cb7b78bd60255eed2a9b0ce5049a62dc5d8a" - integrity sha512-hz31Yma/HSdu9tRS/05xNXY+YuaHOadD9Gd+eh7ankNlJJOFI4IRV6F8BMnloeWYedNXBSTp4susFLwJNQQ+0w== +"@elastic/charts@^17.1.1": + version "17.1.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-17.1.1.tgz#28df3d1be445aa4951fac59dec0831760a1ea034" + integrity sha512-zI3ZHy51tqlsEenDbHFWlL6qbDMvrQXeuV+UFleILtVcfI0r4Lk7DtSbx4GQlHcBuVvvAk1MRdYKNGA0IYWP6w== dependencies: "@types/d3-shape" "^1.3.1" classnames "^2.2.6" @@ -1892,14 +1892,11 @@ d3-color "^1.4.0" d3-scale "^1.0.7" d3-shape "^1.3.4" - fast-deep-equal "^3.1.1" - konva "^4.0.18" newtype-ts "^0.2.4" + path2d-polyfill "^0.4.2" prop-types "^15.7.2" re-reselect "^3.4.0" - react-konva "16.10.1-0" react-redux "^7.1.0" - react-spring "^8.0.8" redux "^4.0.4" reselect "^4.0.0" resize-observer-polyfill "^1.5.1" @@ -19008,11 +19005,6 @@ known-css-properties@^0.3.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4" integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ== -konva@^4.0.18: - version "4.0.18" - resolved "https://registry.yarnpkg.com/konva/-/konva-4.0.18.tgz#43e614c9b22827183506d4a6b3b474f90187b469" - integrity sha512-Tlq0v7QHr8q73xr1cKjHdQl41oHC06IOldPO+ukjt99G74NgoU0TVouvPIFpW2whA9t3xNk/+/VJcc3XPcboOw== - kopy@^8.2.0: version "8.2.5" resolved "https://registry.yarnpkg.com/kopy/-/kopy-8.2.5.tgz#6c95f312e981ab917680d7e5de3cdf29a1bf221f" @@ -19812,10 +19804,10 @@ lodash@^3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -"lodash@npm:@elastic/lodash@3.10.1-kibana3": - version "3.10.1-kibana3" - resolved "https://registry.yarnpkg.com/@elastic/lodash/-/lodash-3.10.1-kibana3.tgz#c0e318245219eeeff535895c429e0cef5058a9ad" - integrity sha512-HMfwwT2yAkEQNzHSR1BxgE5YcDMUaZ/skhNyjy1nvM/A4m0Kh940hLZeCqKBCsSaUJz/8A/9cQGd9BaAOCIBLg== +"lodash@npm:@elastic/lodash@3.10.1-kibana4": + version "3.10.1-kibana4" + resolved "https://registry.yarnpkg.com/@elastic/lodash/-/lodash-3.10.1-kibana4.tgz#d491228fd659b4a1b0dfa08ba9c67a4979b9746d" + integrity sha512-geQqXd9ZedRCL+kq5cpeahYWYaYRV0BMXhCwzq4DpnGCVs430FTMS3Wcot3XChZZhCvkwHm15bpNjB312vPxaA== log-ok@^0.1.1: version "0.1.1" @@ -22917,6 +22909,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path2d-polyfill@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-0.4.2.tgz#594d3103838ef6b9dd4a7fd498fe9a88f1f28531" + integrity sha512-JSeAnUfkFjl+Ml/EZL898ivMSbGHrOH63Mirx5EQ1ycJiryHDmj1Q7Are+uEPvenVGCUN9YbolfGfyUewJfJEg== + pathval@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" @@ -24504,14 +24501,6 @@ react-is@~16.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== -react-konva@16.10.1-0: - version "16.10.1-0" - resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-16.10.1-0.tgz#f8cc2c95374933069e891a6c714c70d0fdc77e68" - integrity sha512-N0Zi3TcWmUxb2d7y1DUDQhRA+WIcqk54DQmmUmJSadj+fS0bg6iZDebQSEQC8dMbjnLHc/338xRT4a4718PEiw== - dependencies: - react-reconciler "^0.22.1" - scheduler "^0.16.1" - react-lib-adler32@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/react-lib-adler32/-/react-lib-adler32-1.0.3.tgz#63df1aed274eabcc1c5067077ea281ec30888ba7" @@ -24647,16 +24636,6 @@ react-portal@^3.2.0: dependencies: prop-types "^15.5.8" -react-reconciler@^0.22.1: - version "0.22.2" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.22.2.tgz#e8a10374fec8fee7c5cd0cf3cd05626f1b134d3e" - integrity sha512-MLX5Y2pNLsdXzWz/GLNhhYkdLOvxEtw2IGqVCzkiRdSFSHRjujI9gfTOQ3rV5z8toTBxSZ2qrRkRUo97mmEdhA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.16.2" - react-redux@^5.0.7, react-redux@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" @@ -24798,14 +24777,6 @@ react-sizeme@^2.6.7: shallowequal "^1.1.0" throttle-debounce "^2.1.0" -react-spring@^8.0.8: - version "8.0.20" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.20.tgz#e25967f6059364b09cf0339168d73014e87c9d17" - integrity sha512-40ZUQ5uI5YHsoQWLPchWNcEUh6zQ6qvcVDeTI2vW10ldoCN3PvDsII9wBH2xEbMl+BQvYmHzGdfLTQxPxJWGnQ== - dependencies: - "@babel/runtime" "^7.3.1" - prop-types "^15.5.8" - react-sticky@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/react-sticky/-/react-sticky-6.0.3.tgz#7a18b643e1863da113d7f7036118d2a75d9ecde4" @@ -26570,14 +26541,6 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -scheduler@^0.16.1, scheduler@^0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.2.tgz#f74cd9d33eff6fc554edfb79864868e4819132c1" - integrity sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"